カテゴリー
【簡単nodejsアプリ開発】NexeでCLI版スネークゲームを作ってみる・後編
※ 当ページには【広告/PR】を含む場合があります。
2021/12/07
スネークゲームの実装
//👇プロセス終了処理(コールバックで)
process.on("exit", e => {
//👇カーソルを表示状態に戻す
process.stdout.write("\x1b[?25h");
//👇画面を全てリセットしたい場合に利用
//process.stdout.write("\x1b[2J");
//👇ゲーム終了時にスコアだけ残したい場合に利用
process.stdout.write("\x1b[1E");
process.stdout.write("\x1b[J");
});
//Ctrl+Cでの終了処理
process.on("SIGINT", () => process.exit(0));
//👇標準出力の文字エンコード
process.stdin.setEncoding('utf8');
//👇一文字ずつ入力
process.stdin.setRawMode(true);
//👇キー入力待ち状態にする
process.stdin.resume();
//👇標準出力の1文字を延々と受け続ける関数オブジェクト(非同期)
const inputKey = (prompt) => new Promise(resolve => {
const isRow = process.stdin.isRaw;
const callBack = key => {
process.stdin.off('data', callBack);
process.stdin.pause();
process.stdin.setRawMode(isRow);
resolve(key);
};
process.stdin.setRawMode(true);
process.stdin.resume();
process.stdin.on('data', callBack);
});
//👇ゲーム画面を描画処理する関数オブジェクト
const refreshGame = () => {
return (currentState, height, timerIdRef) => {
//👇描画開始時にカーソルを一番上の左側に移動
process.stdout.write(`\x1b[${height+1}F`);
if (currentState) {
//👇更新する内容があった場合に描画する
process.stdout.write(`${currentState}`);
} else {
//👇更新する内容が無い場合にはタイマーを止め、プログラム終了
clearInterval(timerIdRef);
process.exit(2);
}
};
};
//👇ゲーム状態を組み立てる関数オブジェクト
// widthとheightで画面のサイズ調整可能
const makeState = (width, height) => {
//👇画面の文字色・背景色
const defaultStyle = '\x1b[42;30m';
//👇ヘビやエサなどのシンボルを文字に置き換え
const spaceChar = ' ';
const snakeHead = '@';
const snakeBody = 'o';
const appleChar = '\x1b[31m' + 'b' + defaultStyle;
//👇ゲームのスコア
let score = 0;
//👇ゲームの終了フラグ
let isGameover = false;
//👇ヘビの初期位置
let snakeX = [Math.floor(width/2)];
let snakeY = [Math.floor(height/2)];
//👇エサをランダムに配置させる
let appleX, appleY;
const genApplePosition = () => {
appleX = Math.floor(Math.random() * width);
appleY = Math.floor(Math.random() * height);
}
genApplePosition();
//👇ゲーム画面(文字列)の組立
const composeGame = () => {
let result = '\n' + defaultStyle;
for (let i=0;i<height;i++) {
for (let j=0;j<width;j++) {
let tile = spaceChar;
if (i == appleY && j == appleX) tile = appleChar;
if (snakeX.length > 1) {
for (let k=1;k<snakeX.length;k++) {
if (i == snakeY[k] && j == snakeX[k]) {
tile = snakeBody;
}
}
}
if (i == snakeY[0] && j == snakeX[0]) {
tile = snakeHead;
if (i == appleY && j == appleX) {
score++;
snakeX.push(appleX);
snakeY.push(appleY);
genApplePosition();
}
}
result += tile;
}
result += '\n';
}
return result + '\x1b[m';
};
//👇ゲーム描画内容を更新
let gameContent = composeGame();
//👇ヘビのキーボード操作を処理する関数オブジェクト
//u | UPキー(%1B%5BA)
//d | DOWNキー(%1B%5BB)
//r | RIGHTキー(%1B%5BC)
//l | LEFTキー(%1B%5BD)
//初回は上方向に移動
let snakeDirection = 'u';
const selectDirection = (key) => {
if (/%1B%5BA/.test(`${key}`)) {
if (snakeX.length > 1 && snakeDirection == 'd') {
isGameover = true;
}
snakeDirection = 'u';
return 'u'
} else if (/%1B%5BB/.test(`${key}`)) {
if (snakeX.length > 1 && snakeDirection == 'u') {
isGameover = true;
}
snakeDirection = 'd';
return 'd'
} else if (/%1B%5BC/.test(`${key}`)) {
if (snakeX.length > 1 && snakeDirection == 'l') {
isGameover = true;
}
snakeDirection = 'r';
return 'r'
} else if (/%1B%5BD/.test(`${key}`)) {
if (snakeX.length > 1 && snakeDirection == 'r') {
isGameover = true;
}
snakeDirection = 'l';
return 'l'
} else {
return ''
}
};
//👇ヘビの座標値を更新する関数オブジェクト
const updateXY = () => {
for (let i=snakeX.length-1;i>=0;i--) {
switch(snakeDirection) {
case 'u':
if (i>0) {
snakeX[i] = snakeX[i-1];
snakeY[i] = snakeY[i-1];
} else {
snakeY[0]--;
if (snakeY[0] < 0) {
snakeY[0]++;
isGameover = true;
}
}
break;
case 'd':
if (i>0) {
snakeX[i] = snakeX[i-1];
snakeY[i] = snakeY[i-1];
} else {
snakeY[0]++;
if (snakeY[0] >= height) {
snakeY[0]--;
isGameover = true;
}
}
break
case 'r':
if (i>0) {
snakeX[i] = snakeX[i-1];
snakeY[i] = snakeY[i-1];
} else {
snakeX[0]++;
if (snakeX[0] >= width) {
snakeX[0]--;
isGameover = true;
}
}
break;
case 'l':
if (i>0) {
snakeX[i] = snakeX[i-1];
snakeY[i] = snakeY[i-1];
} else {
snakeX[0]--;
if (snakeX[0] < 0) {
snakeX[0]++;
isGameover = true;
}
}
break;
default:
break;
}
}
};
//👇ヘビが自分自身への衝突判定をする関数オブジェクト
const checkCollision = () => {
if (snakeX.length > 1) {
for (let i=1;i<snakeX.length;i++) {
if (snakeX[0] == snakeX[i] && snakeY[0] == snakeY[i]) {
isGameover = true;
}
}
}
};
return (currentKey) => {
if (currentKey) {
//👇キーが何かしら押された場合にヘビの進行方向を更新
selectDirection(escape(currentKey));
} else {
//キー入力が空の場合、ゲーム状態の更新
updateXY();
checkCollision();
gameContent = `SCORE >> ${score}` + composeGame();
}
return isGameover ? null : gameContent;
}
};
//👇メイン処理
(async () => {
//👇画面の大きさと描画更新間隔時間を設定
const width = 25, height = 18, speed = 100;
const rg = refreshGame();
const ms = makeState(width, height);
const timerId = setInterval(() => rg(ms(), height, timerId), speed);
//👇カーソルを隠す
process.stdout.write( "\x1b[?25l" );
//👇ゲームを連続で遊ぶ場合打ち込んだコマンド分一つ上にカーソルを戻す
process.stdout.write(`\x1b[1F`);
//👇カーソル以下の行を消去
process.stdout.write("\x1b[K");
let key;
//👇無限ループ処理で、qキーを押せば終了
while (['q'].indexOf(key) < 0) {
key = await inputKey('');
ms(key);
//Ctrl+C(\u0003)がキー入力されたらタイマーを止め、ループを抜ける
if (key === "\u0003") {
clearInterval(timerId);
process.exit(1);
};
};
//👇タイマーを止める
clearInterval(timerId);
//👇プロセス終了
process.exit(0);
})();
snakeX
snakeY
完成したゲームを試す
snake_nexe
snake_nexe
$ ./snake_nexe
まとめ
記事を書いた人
ナンデモ系エンジニア
主にAngularでフロントエンド開発することが多いです。 開発環境はLinuxメインで進めているので、シェルコマンドも多用しております。 コツコツとプログラミングするのが好きな人間です。
カテゴリー