LoginSignup
8
5

More than 5 years have passed since last update.

ブラックジャック JavaScript 90行

Last updated at Posted at 2018-04-23

はじめに

バズってる記事

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();
8
5
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
8
5