使用PHP创建游戏第2部分:井字游戏
在我上一篇有关用PHP创建命令行游戏的文章之后,我们现在有了一种侦听按键的机制。从这里开始的下一步是创建一个简单的游戏。在考虑了适合命令行的游戏之后,我认为井字游戏(也称为noughts和crosss)之类的简单内容将是一个不错的起点。游戏板很小,获胜的条件很容易理解。
尽管井字游戏是一款两人游戏,但我不会添加任何AI控制的玩家,但每位玩家只会轮流放置自己的棋子。我在这里不会做任何花哨的事情,因此不会有任何对象或其他最佳实践技术。这更多是尝试使游戏正常运行的概念证明,因此将有一些关键功能可以抽象出重复的概念,并将某些全局变量传递给每个功能。
让我们从游戏网格本身开始。Tictactoe是在3x3的网格上播放的,因此要表示在代码中,我们只需要一个由三个数组组成的二维数组,每个数组中包含三个项。这种格式允许我们使用字符串存储每个单元的状态。因此,当玩家轮到自己时,阵列中的一项将被更改为“X”或“O”。
$state = [ ['', '', ''], ['', '', ''], ['', '', ''], ];
为了使此功能有效,我们需要定义其他两个变量。当前播放器以及活动单元是什么。活动单元格是我们如何在游戏板上浏览以选择我们要放置令牌的单元格的方式。没有这个,我们将需要一些难以使用的网格参考界面,因为我们不能在命令行中使用鼠标。由于活动单元是对二维数组中某项的引用,因此我们需要为x坐标分配一部分,为y坐标分配一部分。
$player = 'X'; $activeCell = [0 => 0, 1 => 0];
定义好这些变量后,我们就可以开始绘制游戏板了。我们需要做的就是遍历多维数组,并将其转换为一个3x3的网格,以字符串表示。还需要考虑活动单元和玩家变量以更改板的输出。尽管玩家变量只是打印出当前玩家,但活动单元格用于突出显示网格中的一个单元格。
以下函数将接受$stage,$activeCell和$player变量,并返回包含当前播放状态的字符串。这可能比需要的更加冗长,但是清楚地显示了这里发生的事情。
function renderGame($state, $activeCell, $player) { $output = ''; $output .= 'Player:' . $player . PHP_EOL; foreach ($state as $x => $line) { $output .= '|'; foreach ($line as $y => $item) { //选择单元格的当前内容。 switch ($item) { case ''; $cell = ' '; break; case 'X'; $cell = 'X'; break; case 'O'; $cell = 'O'; break; } if ($activeCell[0] == $x && $activeCell[1] == $y) { //突出显示活动单元格。 $cell = '-'. $cell . '-'; } else { $cell = ' ' . $cell . ' '; } $output .= $cell . '|'; } $output .= PHP_EOL; } return $output; }
要运行此命令,我们只需传入前面定义的变量并打印输出即可。
echorenderGame($state,$activeCell,$player);
这将产生以下结果。
Player:X |- -| | | | | | | | | | |
就其本身而言,这并没有太大作用。实际上,PHP脚本将启动,打印游戏网格并完成,但这并不能构成一个非常令人兴奋的游戏。因此,我们需要一种在相同空间中将游戏状态重复输出到命令行中的方法。事实证明,我以前曾在命令行上使用PHP运行Conways的生活游戏时曾考虑过这样做。我们需要的是一个无限循环和一种在每次创建游戏输出时重置命令行的方法。这可以通过使用while循环和调用系统命令“clear”来实现。设置好此位置后,每次渲染游戏输出时,它将位于屏幕顶部,从而给游戏输出动画设置了错觉。
下面的命令将打印相同的游戏网格,但是它将始终位于命令行输出的顶部。
while (1) { system('clear'); echo renderGame($state, $activeCell, $player); }
现在,我们需要某种方式允许玩家在棋盘上移动并选择他们的移动。这是通过听输入的键并相应地对该输入进行操作来完成的。这里需要遵循一些简单的规则。
如果用户输入箭头键,则相应地更新活动单元。不允许活动单元超出游戏网格的范围。
如果用户按下Enter键,并且当前活动单元格为空白,则使用当前玩家的令牌填充该单元格。如果发生这种情况,请交换活动的播放器。
有了这些规则,我们就可以使用translateKeypress()我以前的文章中的函数来读取按键并将其转换为可读性很好的字符串。使用此按键,我们可以按不同的方式来更新变量。请注意,用户动作可以更改状态,活动单元格中的任何玩家变量,因此我们必须通过将变量传递给函数来在函数范围之外进行更改。
function move($stdin, &$state, &$activeCell, &$player) { $key = fgets($stdin); if ($key) { $key = translateKeypress($key); switch ($key) { case "UP": if ($activeCell[0] >= 1) { $activeCell[0]--; } break; case "DOWN": if ($activeCell[0] < 2) { $activeCell[0]++; } break; case "RIGHT": if ($activeCell[1] < 2) { $activeCell[1]++; } break; case "LEFT": if ($activeCell[1] >= 1) { $activeCell[1]--; } break; case "ENTER": case "SPACE": if ($state[$activeCell[0]][$activeCell[1]] == '') { $state[$activeCell[0]][$activeCell[1]] = $player; if ($player == 'X') { $player = 'O'; } else { $player = 'X'; } } break; } } }
为了将其集成到我们的循环中,我们在move()函数之前调用函数,renderGame()以便游戏板将根据用户所做的任何更改进行更新。玩家现在可以在游戏板上移动并放置令牌。
while (1) { system('clear'); move($stdin, $state, $activeCell, $player); echo renderGame($state, $activeCell, $player); }
这里还有一个问题要解决,那就是谁真正赢得了比赛。幸运的是,计算井字游戏的获胜状态相对简单。我们只需要检查水平,垂直和对角线位置上连续三个标记行的存在。如果在检查了这些状态之后没有发现任何东西,那么我们可能正在查看绘制条件,因此我们也需要检查该状态。
如果找到一行,那么我们就退出程序并打印获胜状态的结果。再次,这可能比需要的更为冗长,因为不一定需要多次遍历状态数组,但可以清楚地表明所涉及的步骤。
function isWinState($state) { foreach (['X', 'O'] as $player) { foreach ($state as $x => $line) { if ($state[$x][0] == $player && $state[$x][1] == $player && $state[$x][2] == $player) { //找到水平行。 die($player . ' wins'); } foreach ($line as $y => $item) { if ($state[0][$y] == $player && $state[1][$y] == $player && $state[2][$y] == $player) { //找到垂直行。 die($player . ' wins'); } } } if ($state[0][0] == $player && $state[1][1] == $player && $state[2][2] == $player) { //找到了从左上到右下的对角线。 die($player . ' wins'); } if ($state[2][0] == $player && $state[1][1] == $player && $state[0][2] == $player) { //找到了从左下到右上的对角线。 die($player . ' wins'); } } //游戏可能会平局。 $blankQuares = 0; foreach ($state as $x => $line) { foreach ($line as $y => $item) { if ($state[$x][$y] == '') { $blankQuares++; } } } if ($blankQuares == 0) { //如果没有空白方块,并且没有找到其他任何东西,那么这就是平局。 die('DRAW!'); } }
我们通过用户移动(使用move()功能)并打印出游戏板(使用renderGame()功能)查看获胜状态。这样,当程序退出时,将保留游戏板,并在下面打印最终结果。这是我们的循环现在的样子。
while (1) { system('clear'); move($stdin, $state, $activeCell, $player); echo renderGame($state, $activeCell, $player); isWinState($state); }
在玩完游戏之后,我们看到了以下结果。
Player:O |-X-| X | O | | O | X | O | | X | O | X | X wins%
这是井字游戏,完全在命令行上用PHP运行,虽然看上去有些粗糙,但它的功能就像井字游戏一样。这里有个小问题是,尽管说“X获胜”,但当前玩家是“O”。这是因为当我们运行该move()函数时,我们会交换当前玩家,因此当我们到达获胜状态时,我们已经将新游戏网格呈现出来,下一位玩家成为当前玩家。不过,它仍然可以正确识别获胜的玩家。
实际上,此处使用的无限循环方法已在大多数计算机程序中广泛使用,尤其是需要恒定时间签名才能使所有内容协同工作的游戏。在这里指出这一概念至关重要,特别是如果您习惯于将PHP用作Web开发语言时。大多数时候,Web开发人员非常小心,不允许发生无限循环,因为这会导致网站崩溃。另一方面,游戏开发人员将大部分时间都花在无限循环内,因此对这一概念非常熟悉。
如果您想完整地查看此代码,那么我已经创建了一个GitHubgist,您可以下载并运行它。
在我的下一篇文章中,我将介绍一些更具野心的东西,包括实时游戏性和图形。好吧,在命令行界面的限制范围内...