使用 XForms 库编程,第 2 部分:编写应用程序

作者:Thor Sigvaldason

上个月,我们开始了关于 XForms 的系列文章,解释了如何安装表单库和包含文件。我们还通过编写几个简单的程序,尝试了使用 XForms 进行编程。在本月的文章中,我们将编写一个功能完善的应用程序。我们将从解释项目开始,然后看看如何使用 XForms 实现它。

项目:博弈论模拟器

我们的任务是实现一个博弈论模拟器。如果您碰巧没有数学博士学位,您可能需要看看本杂志第 52 页上的 博弈论入门。我们正在攻克一项不简单的编程任务,以便掌握如何使用 XForms 进行“真实世界”的编程。您不必理解博弈论的每一个细微之处,因为我们的主要目标是弄清楚 XForms。

在一个正式的博弈中,我们必须考虑两个主要实体:参与者和收益。因此,我们的模拟器应该允许我们为这些元素设置相关的值。例如,参与者由行动和策略定义。同样,收益只是一组参与者在博弈时获得的价值。为了极大地简化问题,我们假设只有两种类型的参与者和两种可能的行动。这降低了我们编程问题的维度。对于读者来说,一个好的练习是尝试放宽两种策略的限制,但在尝试修改它之前,请确保您相当好地理解了初始程序。

由于我们正在创建一个图形应用程序,我们希望有一个点击式界面来设置我们的参与者。采用的方法是将参与者视为具有有限数量的状态。在每个状态中,都有一个要采取的行动。由于我们将自己限制为只有两种可能的行动,让我们将它们称为 A 和 B。我们的模拟器将用于重复博弈,因此我们希望能够设计可以改变其行动的参与者。也就是说,移动到另一个状态,该状态告诉他们执行不同的行动。只有两种类型的参与者,列玩家和行玩家,以及两种行动,因此要采取哪个行动的选择只能受到另一位玩家所做的事情的影响。

假设您想设计一个始终执行行动 A 的玩家。这很简单;只需将每个状态中的行动设置为 A 即可。一个更复杂的例子是设计一个玩家,如果另一位玩家上期选择 A,则执行 A,如果另一位玩家选择 B,则执行 B。用图表更容易看出这一点

Programming with the XForms Library, Part 2: Writing an Application

图 1. 玩家状态图

在这里我们看到,从一个状态到另一个状态的转换可以取决于其他玩家的行为。因此,为了实现设计玩家的界面,我们必须能够指定每个状态中的行动以及要执行的转换,即,给定另一位玩家的行为,要跳转到哪个状态。我们不能使一位玩家的选择取决于另一位玩家在当前时期所做的事情。这将违反标准博弈论的原则之一:同时选择行动。这里我们只显示了两种状态,但我们将在实际程序中允许更复杂的玩家策略。

我们还希望每种类型都有多个玩家存在,并且随机与另一种类型的玩家匹配。这听起来比实际情况更困难,因为我们仍然只需要设计两种类型的玩家。例如,行玩家群体应该仅在他们当前所处的状态上有所不同,而不是在他们的策略的总体设计上有所不同。

与玩家设计一样,我们希望用户能够轻松设置和编辑收益。这稍微简单一些,因为我们只需要收益表的图形表示以及让用户更改这些值的方法。我们希望这两个功能都出现在它们自己的窗口中,以便我们可以在需要时弹出它们。

一旦用户指定了玩家策略和收益,我们还需要一种实际运行模拟的方法。此例程应匹配玩家、分配他们的收益并处理从一个状态到另一个状态的转换。它还应该让我们设置游戏应该运行多长时间,并为我们提供一些关于游戏进度的良好视觉反馈。

所有这些输入、交互和编辑可能看起来非常复杂。如果我们要在简单的终端窗口上编程这个项目,它可能需要一套相当繁琐的菜单或命令语言。但是使用 XForms,我们可以轻松创建窗口、输入字段和实现我们的博弈论模拟器所需的其他图形元素。如果这一切有点模糊,那么最好先玩一下正在运行的程序(见下文),然后再回到本节。

xgtsim 程序

让我们直接深入。我们将立即启动并运行一个示例,然后使用本文的其余部分来解释它是如何工作的。该程序名为 xgtsim,C 源代码可以在列表 1.1 中找到。虽然欢迎您键入它,但它也可以在本系列的网站上找到(请参阅 http://a42.com/~thor/xforms,列表 1)。它应该可以使用以下命令编译

gcc -lX11 -lforms -lm xgtsim.c -o xgtsim

在 X Window 系统中,您应该能够通过在 xterm 窗口中键入 ./xgtsim 来运行该程序。如果您遇到问题,您可能需要返回并查看上个月关于安装 XForms 的文章。在所有可能的窗口都打开的情况下,运行的程序应该类似于图 2。

Programming with the XForms Library, Part 2: Writing an Application

图 2. 窗口打开的 xgtsim

如果您想在继续阅读本文的其余部分之前试用该程序,一个有用的练习是设置一个囚徒困境。只需使用收益编辑器将值设置为与入门手册中显示的值相同,然后尝试一些不同的玩家策略。特别是,尝试弄清楚当两个以牙还牙策略相互对抗时会发生什么。他们玩的初始策略重要吗?

程序的流程

上个月,我们看到设计 XForms 程序的基本步骤如下

  1. 包含 forms.h 以访问 XForms 例程

  2. 尽快调用 fl_initialize()

  3. 通过创建表单来设置您的图形界面

  4. 通过设置回调为相关对象分配操作

  5. 显示一个或多个表单

  6. 将控制权交给 fl_do_forms()

我们在 xgtsim 中使用了这种方法。像所有 C 程序一样,执行从 main() 例程开始,该例程位于源代码的末尾。首先我们调用 fl_initialize() 来设置 XForms,并允许它解析命令行参数。接下来,我们调用 set_defaults(),它为随机数生成器播种,并为我们的收益和玩家设计变量设置一些默认值:payoffs[][][]state_actions[][]state_transitions[][][]

然后调用 create_forms(),它设置我们所有的窗口、图形元素和回调。我们稍后将详细介绍,但让我们看看它对于最简单的情况是如何工作的:退出程序。在 create_forms() 代码中,我们使用 main_window(类型为 FL_FORM 的变量)创建一个窗口,该窗口将在程序启动时显示。此窗口上有四个按钮,分别名为 Players、Payoffs、Run 和 Quit。请注意,Quit 按钮设置为使用以下命令调用函数 quit_xgtsim()

fl_set_object_callback(obj, quit_xgtsim, 1);

这意味着每当鼠标单击标记为 Quit 的按钮时,将调用 quit 例程。此函数反过来只是调用 fl_finish() 然后退出。

回到 main() 函数的流程,在使用 create_forms() 设置我们所有的窗口、按钮等等之后,我们然后通过调用 fl_show_form() 使我们的 main_window 出现。然后我们通过调用 fl_do_forms() 将控制权交给用户。

至关重要的是要理解,在 create_forms() 中设置我们的表单不仅仅是决定图形应该如何在屏幕上布局。通过设置回调以将按钮按下和数据输入与特定操作链接起来,我们实际上已经设置了程序的整个流程。当用户按下 Payoffs 按钮时,是 XForms(通过 fl_do_forms())调用相关例程以使 Payoffs 窗口出现,并处理与该窗口的后续交互。事实上,如果我们正确设置了所有回调,执行永远不会返回到 main()。只有当用户激活没有与之关联的回调的对象时,fl_do_forms() 例程才会返回。

一些细节

由于 create_forms() 非常重要,让我们更详细地看看它。我们使用并重用一个名为 *obj 的通用指针,它的类型为 FL_OBJECT,来创建我们的许多图形元素。这可能有点令人困惑,但我们将随着我们的进行澄清事情。

create_forms() 中创建的第一个表单是 main_window。这是一个全局指针变量,我们在源代码的早期声明它。我们告诉 XForms 它是一个窗口,其宽度应为 290 像素,高度应为 50 像素,并使用以下赋值

main_window = fl_bgn_form(FL_NO_BOX, 290, 50);

在接下来的九行代码中,我们创建了四个按钮,这些按钮将用于弹出窗口以进行用户交互并退出程序。每次我们需要一个新的图形元素时,我们都只使用 obj,这节省了内存并使事情保持简单。只需记住,每当我们重新分配 obj 时,所有将 obj 作为值传递的后续函数都将影响最近的赋值。Players、Payoffs 和 Run 按钮都通过回调链接到名为 display_forms() 的例程,但它们被设置为使用值 1、2 和 3 分别调用该例程。display_forms() 例程反过来使用这些值来决定要显示哪个窗口。在创建 Quit 按钮后,我们通过调用 fl_end_form() 告诉 XForms 我们已完成向此表单添加元素。

然后我们继续创建 player_windowpayoff_windowrun_window。这些都遵循相同的通用模式;使用 fl_bgn_form() 声明窗口的尺寸,添加我们想要的尽可能多的对象(在我们进行时分配回调),然后使用 fl_end_form() 完成。我们将详细查看 run_window,因为它最简单。一旦你弄清楚它,你可能想自己查看其他两个。

由于我们希望从游戏中获得视觉反馈,我们在 run_window 中创建了两个图表。我们通过指定 FL_LINE_CHART 将它们制成折线图,并通过包含 4 个整数值来设置尺寸。前两个值表示我们的图表的左上角应该出现的位置,其中 0,0 是创建对象的表单的最左上角。接下来的值描述了对象的宽度和高度。最后,我们提供一个字符串来为图表提供标签

column_chart = fl_add_chart(FL_LINE_CHART, 10, 30,
        190, 90, "Column Players");

您可能想知道为什么我们将此函数调用分配给名为 column_chart 的变量而不是使用我们的通用 obj 变量。这样做是因为 column_chart 被声明为全局变量,xgtsim 中的所有例程都可以访问它。特别是,当游戏实际运行时,play_the_game() 例程使用这个全局变量来向我们刚刚创建的图表添加值——查找函数 fl_add_chart_value()

由于标签 "Column Players" 被分配给我们的图表,默认行为是让它出现在图表下方。我们通过调用 fl_set_object_lalign() 将其移动到左上角。然后我们使用 fl_set_chart_maxnumb() 限制可以显示的项目数量。然后我们创建一个几乎相同的图表来显示关于行玩家的信息。

除了图表反馈之外,我们还创建了两个浏览器来显示数值数据。这是通过调用 fl_add_browser() 完成的。浏览器是 XForms 中非常有用的对象,它们可以以许多不同的方式使用。我们在这里的实现非常简单,但您可以在 XForms 文档中了解更多关于它们的信息。

为了允许用户设置游戏应该运行的迭代次数,我们创建了一个计数器,并设置了许多选项。首先我们将标签对齐到左侧显示,然后将精度设置为 0。这只是意味着我们希望我们的计数器保存整数数据,因为您不能真正执行半次迭代。标准计数器在屏幕上显示两组箭头。每当按下它们时,它们都会更改计数器正在保存的数值数据的值。我们使用调用 fl_set_counter_bounds() 设置此数据的界限,然后通过设置计数器步进速率,使一组按钮将值更改 1,另一组按钮将值更改 100。我们还在计数器中将起始值设置为默认值(存储在 numb_iterations 中),然后记录回调。每当计数器对象更改时,都会调用例程 set_iterations(),该例程设置变量 numb_iterations

运行窗口还包含两个按钮,一个用于开始运行游戏,一个用于停止游戏。请注意,我们在表单上的完全相同的位置创建了这两个按钮,以便它们彼此重叠。但是,在完成之前,我们隐藏了 stop_button 以确保 go_button 可见。当按下 go_button 时,它会调用 play_the_game(),这会隐藏 go_button 并使 stop_button 出现。调用 fl_hide_object()fl_show_object() 的能力使 XForms 中的表单设计非常灵活,因为您可以设计窗口,其中对象根据任意数量的条件出现和消失。当对象被隐藏时,用户不可能激活它。

一旦调用了 fl_end_form(),我们就快完成了这个窗口。但是,紧接着,我们调用

fl_set_form_atclose(run_window, close_forms, 0);

这告诉 XForms 当窗口管理器发送关闭窗口信号时要运行哪个例程。在大多数窗口管理器上,当用户单击相关窗口标题栏中的关闭图标时,会发送此信号。这就像回调,但格式略有不同。在正常回调中,声明的形式为

fl_set_object_callback(the_object, the_function,
         an_argument);
the_function 指向的函数必须接受两个参数,一个 FL_OBJECT 指针和一个 long,如
the_function(FL_OBJECT *obj, long an_argument);
此函数必须返回 void。但是,当窗口关闭信号发送时,它适用于整个窗口/表单,而不是该表单上的特定对象。因此,fl_set_form_atclose() 的第一个参数必须是指针类型 FL_FORM,如
close_forms(FL_FORM *form, void *an_argument);
此函数必须返回一个整数,特别是,如果您希望窗口实际关闭,它应该返回 FL_OK,如果您希望窗口保持可见,则应返回 FL_IGNORE

在查看了 main()create_forms() 之后,源代码的其余部分就很容易理解了。最复杂的部分是 player_window 如何使用 row_or_column 变量在单个表单上编辑两种类型的玩家。总体思路如下。全局变量 state_actions[][]state_transitions[][][] 保存关于两种类型玩家的当前状态的所有数据,即,列玩家和行玩家。在 Player 窗口上,有两个按钮允许用户选择他们想要编辑的玩家类型。每当按下这些按钮时,必须更新 Player 窗口以反映这些变量的状态。这是通过 set_row_or_column() 例程完成的,该例程从 state_actions[][]state_transitions[][][] 中读取值到 Player 窗口上的相关对象中,这些对象是 action_choices[]transition_inputs[][]

在窗口更新以反映相关玩家集的当前状态后,用户现在可以编辑这些值。这是通过 set_player_values() 函数完成的,该函数在任何这些屏幕对象更改时调用。我们不费心弄清楚哪个对象被更改,而是简单地将 Player 窗口上的所有值读取到 state_actions[][]state_transitions[][][] 中。

程序中唯一剩下的微妙之处是 abort_flag 变量的使用以及在 play_the_game() 的迭代循环中调用 fl_check_forms()。当在 Run 窗口中激活 Go 按钮时,将调用 play_the_game()。在该例程中完成的第一件事是将 abort_flag 设置为零。玩家被匹配,收益被支付,并且 Run 窗口上的图表被更新。在迭代循环的底部,我们检查 abort_flag 是否已从 0 更改为 1,如果已更改,我们停止运行。您可能会感到困惑,这个标志的值是如何在这个算法中可能发生变化的。

关键在于调用 fl_check_forms()。这是一个非阻塞例程,其工作方式与 fl_do_forms() 完全相同,只是如果没有激活任何对象,它会立即退出。这会带来很小的性能损失,因为程序在游戏运行时有效地监视其所有对象,但这非常值得。由于我们将回调设置为 Stop 按钮以将 abort_flag 更改为 1(通过 stop_the_game()),因此单击 Stop 按钮将导致当前游戏中止。

这样做的好处是允许我们在模拟运行时修改我们所有的数据。例如,我们可以更改收益值,并立即通过 Run 窗口中的视觉反馈看到这如何改变正在展开的游戏。同样,我们可以动态更改玩家策略,并观察这如何影响他们的表现。这种探测和运行时编辑参数通常很难通过在控制台上运行的标准 C 实现,但是通过一些全局变量、一些明智的设计和对 fl_check_forms() 的调用,XForms 使它几乎变得微不足道。

尝试事项

如果您设法仔细研究了 xgtsim 并对正在发生的事情有了很好的了解,您可能想尝试更改源代码以测试您的理解。一个要尝试的事情是在主窗口中添加一个额外的按钮,该按钮可以随机化所有当前变量。也就是说,假设用户设置了收益和策略,但想要打乱这些值。您不仅需要添加按钮并设置回调,还需要更新任何当前显示的窗口以反映这些更改。

Run 窗口中的图表当前仅提供关于两种类型玩家的平均反馈。尝试添加更多图表或其他元素,以显示关于每个类别中最佳和最差玩家的信息。

如果您感觉真的很有野心,那么尝试更改 xgtsim 以允许更多行动和更复杂的策略。这可能会变得非常复杂,因为收益矩阵等元素将不得不根据当前可能的操作数量而增长和缩小。这可以通过动态创建新对象和表单来实现,这是我们到目前为止尚未涵盖的内容。

Programming with the XForms Library, Part 2: Writing an Application

下个月预告

在玩 xgtsim 时,您可能会发现一组策略和收益会产生有趣的结果。目前没有办法保存游戏的这种状态,因为我们没有基于文件的输入和输出。我们将在下个月添加该功能,通过使用 XForms 预构建的文件请求例程。它只是 XForms 包含的一整套“好东西”的一部分,我们将查看其中的大部分。

资源

我们还将使用一些像素图来修饰我们的应用程序,学习如何设置重力参数来控制窗口大小调整,并查看 XForms 的一些其他有趣的功能。

Thor Sigvaldason 是统计程序 xldlas 的作者,该程序使用 XForms 库(参见 LJ #34,1997 年 2 月)。他正在努力完成经济学博士学位,可以通过 thor@netcom.ca 联系到他。

加载 Disqus 评论