book
归档: 娱乐 
flag
mode_edit

来玩蛇吧!(* 为蛇头,# 为蛇身,@ 为食物)

snake

在 HUST 新生群里和学长愉快地吹水的时候,某学长建议新生不要只跟着老师走,可以写一写贪吃蛇,扫雷之类的小游戏玩玩。想了想也觉得挺有意思,就写了一下。

思路大概是这样的,每隔 $\dfrac 1 4$秒刷新一下蛇的位置,在这$\dfrac 1 4$秒中,如果用户按下键盘,那么就以此改变蛇的运动方向。

首先考虑怎么维护蛇的位置,直接维护一个字符数组显然是不明智的。那么,蛇这种曲里拐弯的东西应该怎么处理呢?我开始的时候想存个拐点什么的,后来想了想似乎不太现实。最后的方法是按从头到尾顺序存下蛇身上每个点的坐标。

这样做有什么好处呢?考虑蛇的移动,蛇头向着某方向前进一格,然后紧邻着蛇头的那一格蛇身会占据原来蛇头的位置,以此类推。蛇的前进实际上是数组的位移。因此只需要每次 pop 蛇尾,push_front 蛇头的新位置即可。

解决完蛇的移动,下面我们来考虑蛇的死亡、吃食物判定与反馈。判断蛇是否死亡是容易的,我们只需要在移动后检查数组内是否有重复元素即可。更进一步,考虑到贪吃蛇游戏的性质,只需要判断蛇头是否与蛇的其他部位重合。吃食物也是如此。那么吃食物后的反馈应该是怎么样的呢?根据我依稀的童年回忆,蛇头碰到食物后,食物就会「变成」蛇头。因此只需要 push_front 蛇头的新位置,而不需要 pop_back 了。注意到贪吃蛇游戏实际上是在甜甜圈上进行的,因此需要在坐标超出范围时拨乱反正:把负的加成正的,把大的模成小的。

输入

怎样读到用户输入的 Arrow 键?用常规的 cinscanf 好像不太行。在搜索之后得到答案,应该使用 getch()。它可以读一个字符,而且不回显,就像 Unix 的密码输入一样。通过一些实验可以得到 Arrow 键的键码。这样我们就能读到 Arrow 键了。

但是…考虑我们的流程:在死循环内,每隔一段时间刷新蛇的位置,在这段时间内等待用户的操作。可是,getch() 是阻塞线程的!这可怎么办?幸好,另外一个函数是 kbhit() 如果用户按下了按键,它的返回值就会是 true,这样一来,如果它的返回值是 false,我们就不必去读字符了。

另一个有意思的问题是蛇的方向控制,假设蛇正在向左飞驰,那么——根据我们的经验——按 <-->键都是不起效的。因此,我们还需要特判一下。但是,摞一大坨 if 上去也太不优雅了吧!我的操作是这样的:

enum KeyRes { AUp = 0, ADown = 2, ALeft = 1, ARight = 3, AEsc = -1 };

using point = complex<int>;

map<KeyRes, point> d = {
    {AUp, {-1, 0}}, {ADown, {1, 0}}, {ARight, {0, 1}}, {ALeft, {0, -1}}};

userPress = getKey();
if (d[userPress] + d[lastPress] == point({0, 0})) {
  userPress = KeyRes(((int)userPress + 2) % 4);
}

也就是说,如果两次按键的位移相加为 $\mathbf 0$,那么把按键修正为用户按键的反方向。通过设计 KeyRes 内各个元素的值,恰好可以让这个操作变成 \x -> (x + 2) % 4

输出

一种比较弱智的思路是这样的:每次拿着蛇的位置等数据刷新一帧后,就重画整个屏幕。再重新 print 一遍。傻子都知道,这是非常慢的。如何只修改终端上坐标为 $(x,y)$ 的点的字符呢?这就需要ANSI 转义序列。我们主要用到这样一个:

void moveTo(int x, int y) { cout << "\x1b[" << x << ';' << y << 'H'; }

我们可以拿着新生成的字符数组和旧的比对一下,拿到一系列的 patch ,然后用这些 patch 去刷新屏幕,实现按需刷新。

void applyPatch(const vector<patch> &p) {
  for (int i = 0; i < p.size(); ++i) {
    moveTo(p[i].x + 1, p[i].y + 1);
    cout << p[i].value;
    cout.flush();
  }
}

这样一来,我们就可以愉快地玩🐍了。

完整代码请看 GitHub

写在后面

作为一个前 OIer,我对于玩蛇这种应用性比较强的东西一向是比较鄙视的,毕竟也没有什么算法上的技术含量。但是在写的过程中也的确出了一些问题,比如 getch() 的问题,和清屏、延时之类的问题。不到 200 lines 的东西写了一晚上+半个上午,也算是有点丢人了(