创建一个Node.js井字棋游戏
2021年初对我来说是充满新鲜有趣经历的一年。在新年夜前后,我收到了一封来自某家公司的联合创始人的电子邮件,这家公司是我在2019年申请过的兼职远程JavaScript职位。
长话短说,我去面试了,感觉可能搞砸了(唉,现在没人理我了)。
这是我人生中的第一次面试,因为我之前一直都是自由职业者。我当时想找个机会加入团队,哪怕是兼职也好,这样可以积累一些新的工作经验,学习一些新东西,能应用到我的工作中。当然,额外的收入也是个不错的选择。
正如我之前所说,我觉得面试搞砸了,主要是编程任务没做好。我的任务是在30分钟内编写一个后端井字棋游戏(在终端上运行)。除了判断玩家何时获胜之外,还需要添加一些功能,例如判断平局、设置一个可以撤销上一步操作的按键,以及其他一些我记不清的功能。
这是我第一次接触井字棋游戏,而我自己的另一个失误是没有深入研究Node.js的后端(终端)工具。由于上次使用C++,最近又接触了Rust,所以一直没怎么接触过终端输入,因此在获取终端输入方面遇到了困难。
我花了不少时间才熟悉游戏和用来编写代码的平台(repl.it),因为它们对我来说都是全新的。虽然没能按时完成任务,但之后我还是抽出时间自己完成了,研究了一些关于如何使用Node.js从终端获取输入流的方法,找到了Readline模块,并阅读了一些关于Node.js进程事件的资料。
我很感激这次经历,但唯一不足之处来自面试我的公司。虽然他们没有义务这样做,但考虑到他们承诺在面试后三天内通知我结果,而且我之后也发邮件询问过,如果他们能及时告知我面试状态,无论结果如何,我都会很感激。
好了,废话不多说,让我们进入正题。
我决定和大家分享一下我在采访后开发的井字棋游戏的代码。
你可以把它当作模板,进行改进,纯粹为了娱乐,或者至少如果你是新手,可以从中学习。我确信它还有提升的空间,等我有时间就会着手改进。
我已经添加了处理输入流和完善平局判定机制的代码,有兴趣的朋友可以去GitHub仓库good first issue看看。
xinnks / tictactoe-nodejs
一款专为终端设计的终端井字棋游戏
创建游戏
考虑到职业系统带来的所有优势,我决定游戏应该采用职业系统,而不是到处堆砌独立的功能,因为功能的数量相当多。
const readline = require('readline');
'use strict';
class TicTacToe {
...
}
绘制游戏棋盘:
this.ticTacToeLayout = `${this.displayItem(this.ticTacToe[0])} | ${this.displayItem(this.ticTacToe[1])} | ${this.displayItem(this.ticTacToe[2])}
---------
${this.displayItem(this.ticTacToe[3])} | ${this.displayItem(this.ticTacToe[4])} | ${this.displayItem(this.ticTacToe[5])}
---------
${this.displayItem(this.ticTacToe[6])} | ${this.displayItem(this.ticTacToe[7])} | ${this.displayItem(this.ticTacToe[8])}`;
为了使这篇博客篇幅较短,便于阅读(因为其完整源代码可在GitHub 仓库中找到),我将重点介绍这款游戏的基本部分。
接收输入流:
在类的构造函数中初始化 readline 模块的接口,该模块从可读流(在本例中为 process.stdin)读取数据。
constructor(){
this.rl = readline.createInterface({
input: process.stdin,
output: process.stdout
})
}
在游戏场景中,收集终端输入的最佳方法是监听输入流的结尾。我们可以使用 readline 监听器来监听输入流接收到行尾输入(例如 \n、\r 或 \r\n,即按下回车键或回车键
时发生的行尾输入)时发出的“line”事件。
startGame(){
this.displayLayout();
// listen to inputs
this.rl.on("line", (input) => {
if(this.ticTacToe.length <= 9){
// read move
this.readMove(parseInt(input))
// continue playing
} else {
console.log("Game Ended!");
this.processGame();
}
})
...
}
我们从游戏中收集的第二个输入是监听一个特殊按钮,点击该按钮可以撤销上一步操作。我们在上面的 ` startGame()`
方法 的末尾处理它。
...
// listen to delete events by backspace key
process.stdin.on('keypress', (str, key) => {
// delete move
if(key.sequence === '\b'){
this.deleteLastMove()
}
})
...
游戏中每一步棋都会被记录下来,并添加到名为moveRegister的走棋数组中。deleteLastMove ()方法的作用是从moveRegister中删除最后一步棋,并撤销添加到ticTacToe数组中的最后一个元素,该数组用于在游戏棋盘上绘制X和O字符。
游戏处理中
游戏的另一个关键部分是处理用户输入。
由于游戏棋盘由九个可能的位置组成,用户可以在这些位置上绘制数据,而在井字棋游戏中,第一个用三个自己的字符(X或O)连成一条直线的玩家获胜,因此我们需要在游戏中查找这种情况,即查找同一用户在两个玩家之间连成的所有直线。`processGame ()`方法正是执行此操作。
...
processGame(){
// at least 5 moves need to have been made
if(this.moveRegister.length >= 5){
var checkSet = new Set()
// possible vertical alignments
if(this.ticTacToe[0] && this.ticTacToe[3] && this.ticTacToe[6] && (Array.from(checkSet.add(this.ticTacToe[0]).add(this.ticTacToe[3]).add(this.ticTacToe[6])).length === 1)){
console.log(`Player ${this.getPlayerFromChar(this.ticTacToe[0])} Wins!!`);
this.endGame();
}
checkSet.clear();
if(this.ticTacToe[1] && this.ticTacToe[4] && this.ticTacToe[7] && (Array.from(checkSet.add(this.ticTacToe[1]).add(this.ticTacToe[4]).add(this.ticTacToe[7])).length === 1)){
console.log(`Player ${this.getPlayerFromChar(this.ticTacToe[1])} Wins!!`);
this.endGame();
}
checkSet.clear();
if(this.ticTacToe[2] && this.ticTacToe[5] && this.ticTacToe[8] && (Array.from(checkSet.add(this.ticTacToe[2]).add(this.ticTacToe[5]).add(this.ticTacToe[8])).length === 1)){
console.log(`Player ${this.getPlayerFromChar(this.ticTacToe[2])} Wins!!`);
this.endGame();
}
checkSet.clear();
// possible horizontal alignments
if(this.ticTacToe[0] && this.ticTacToe[1] && this.ticTacToe[2] && (Array.from(checkSet.add(this.ticTacToe[0]).add(this.ticTacToe[1]).add(this.ticTacToe[2])).length === 1)){
console.log(`Player ${this.getPlayerFromChar(this.ticTacToe[0])} Wins!!`);
this.endGame();
}
checkSet.clear();
if(this.ticTacToe[3] && this.ticTacToe[4] && this.ticTacToe[5] && (Array.from(checkSet.add(this.ticTacToe[3]).add(this.ticTacToe[4]).add(this.ticTacToe[5])).length === 1)){
console.log(`Player ${this.getPlayerFromChar(this.ticTacToe[3])} Wins!!`);
this.endGame();
}
checkSet.clear();
if(this.ticTacToe[6] && this.ticTacToe[7] && this.ticTacToe[8] && (Array.from(checkSet.add(this.ticTacToe[6]).add(this.ticTacToe[7]).add(this.ticTacToe[8])).length === 1)){
console.log(`Player ${this.getPlayerFromChar(this.ticTacToe[6])} Wins!!`);
this.endGame();
}
checkSet.clear();
// possible diagonal alignments
if((this.ticTacToe[0] && this.ticTacToe[4] && this.ticTacToe[8] && (Array.from(checkSet.add(this.ticTacToe[0]).add(this.ticTacToe[4]).add(this.ticTacToe[8])).length === 1)) || (this.ticTacToe[2] && this.ticTacToe[4] && this.ticTacToe[6] && (Array.from(checkSet.add(this.ticTacToe[2]).add(this.ticTacToe[4]).add(this.ticTacToe[6])).length === 1))){
console.log(`Player ${this.getPlayerFromChar(this.ticTacToe[4])} Wins!!`);
this.endGame();
}
checkSet.clear();
}
}
...
希望这款游戏的源代码能对你们未来的面试或使用 NodeJs 终端方面的探索有所帮助。
去把航站楼砸了吧。

