はじめに
バズってる記事
JavaScript (というか Nodeで) で解いてみた。
- 90行
- A は 1, 11の両方で評価
- 得点計算、スプリット等の派生ルール等は未実装。
概説
Node ならではの留意点
対話的に作るのが難しくてクソみたいな実装になった。「ユーザからの入力が来るまで同期的に待つ」というのが、非同期を基本とするJavaScriptでは難しい。他の言語なら楽なのに。
具体的には、まず、readline
モジュールを使うことで、標準入力をストリームに変換し、行ごとにロジックを動作させる。この処理が終わるまで次に行かないように無限ループを配置。無限ループは、CPU 使うのはエコじゃないので非同期スリープで実装。無限ループの脱出のために、行が来るごとに起動されるロジックの中でフラグを立てる。
イメージのコードはこう。実際のコードは playerAction()
を参考のこと。
let breakFlag = false;
reader.on('line', (line) => {
if (someCondition) breakFlag = true;
doSomeLogic();
}
// この先に処理が進まないように無限ループで引き留める await を使った非同期スリープ
while (!breakFlag) {
await new Promise(r => setTimeout(() => r(), 100)); // sleep
}
この辺の処理パターンは頻出だろうけど、Nodeでどうやって実装するのが正解なのだろう。ジェネレータを使って書く方がすっきりかけそう。
英語とルールの勉強
変数名に使うので、英語の勉強。
- suit: マーク。ハート、スペード、クラブ、ダイヤの総称
- rank: 数字。1, 2, ... ,10 , J, Q, K を表す
- deck: デッキ。全52枚のカードの集合を表す。
- hand: 手札。
- bust: 22以上になってしまうこと
- hit: 1枚引くこと
- stand: もう引かないよということ
特記すべきデータ構造
- ハンドの point は配列
ハンドの得点の計算方法は複数ある。A を 1 とするか、 11 とするかという任意性があるため。そのため、ハンドの得点というのは、複数あることになるので、配列で実装した。これをポイントと呼んでいる。注: 要素数は 2じゃないよ。Aが何枚か来た時を考えよう。
- ハンドの score は整数
とはいえ、最終的には得点を比較して勝敗を決める。バストを0点、それ以外の場合、pointのうちで21を超えない最大のものを点数とする。これをスコアと呼んでいる。
ずるい?
JavaScript は関数がほとんど存在しないので、shuffle, xprod
を外部ライブラリ(Ramda, Lodash) から輸入した。自前で書くと、コードは20行くらい増える。まあ許して。
オブジェクト指向はどこに行った?
関数型が好きなので、オブジェクト指向にはならなかった。とはいえ、純粋でない(引数の配列を破壊的に操作する)関数もあって、全体的にはちょっと残念なコードになっている。
改良点
- deck を破壊的にする動作をやめて、関数型チックにかく(
playerAction()
,AIAction
) - ジェネレータを使って対話処理部をすっきり書く
- ゲームを何回もしたりすることを考えて、勝敗結果をきちんとした形で整理
ref. match()
コード
const readline = require('readline');
const { xprod } = require('ramda');
const { shuffle } = require('lodash');
const RANK = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13];
const SUIT = ['H', 'D', 'S', 'C'];
const DECK = xprod(RANK, SUIT);
// Hand Stuff
const point = (rank) => { // XXX: point of rank is Array!
switch (rank) {
case 1: return [1, 11];
case 11:
case 12:
case 13: return [10];
default: return [rank];
}
};
const pointOf = (hand) => { // XXX: point of hand is Array!
if (hand.length === 1) return point(hand[0][0]);
const headPoint = point(hand[0][0]);
const tailPoint = pointOf(hand.slice(1, hand.length));
return xprod(headPoint, tailPoint).map(x => x[0] + x[1]);
};
const scoreOf = (hand) => { // 0 on bust
const max = arr => arr.reduce((acc, x) => acc < x ? x : acc, 0);
const score = h => max(pointOf(h).filter(s => s < 22));
return score(hand);
};
const bust = hand => scoreOf(hand) === 0;
const blackJack = hand => scoreOf(hand) === 21;
const match = (you, ai) => {
const [s1, s2] = [scoreOf(you), scoreOf(ai)];
const resultCode = s1 - s2;
const result = resultCode === 0 ? 'Draw' : resultCode > 0 ? 'Player wins' : 'AI wins';
const summary = `${result} (player: ${s1 || 'bust'}, AI: ${s2 || 'bust'})`;
return { resultCode, summary };
};
// AI Stuff
const shouldDraw = hand => scoreOf(hand) < 17;
const AIAction = (deck, initHand) => { // XXX: this alters deck!
const hand = [...initHand];
while (shouldDraw(hand) && !bust(hand)) {
hand.push(deck.shift());
}
return hand;
};
// Player Stuff
const playerAction = async (deck, handPlayer) => { // XXX: this alters deck!
const hand = [...handPlayer];
const reader = readline.createInterface({ input: process.stdin });
console.log('hit or stand [s/H]>');
let breakFlag = false;
reader.on('line', (line) => {
if (line.match(/[sS]/)) {
breakFlag = true;
reader.close();
return;
}
hand.push(deck.shift());
console.log(`Your hand: ${hand.map(x => x.join('')).join(',')}`);
if (bust(hand)) {
breakFlag = true;
reader.close();
}
});
while (!breakFlag) {
await new Promise(r => setTimeout(() => r(), 100)); // sleep
}
return hand;
};
// main
const playGameOneshot = async () => {
const deck = shuffle(DECK);
const initHandAI = deck.splice(0, 2);
console.log('AI draws', initHandAI[0].join(''), '*');
if (blackJack(initHandAI)) {
console.log('AI wins with BlackJack!');
return;
}
const initHandPlayer = deck.splice(0, 2);
console.log('player draw', initHandPlayer[0].join(''), initHandPlayer[1].join(''));
// you play
const handPlayer = await playerAction(deck, initHandPlayer);
if (bust(handPlayer)) {
console.log('AI wins because you busted!');
return;
}
// AI plays
const handAI = AIAction(deck, initHandAI);
const result = match(handPlayer, handAI);
console.log(result.summary);
};
playGameOneshot();