ブラックジャック JavaScript 90行 

# はじめに
バズってる記事

- [プログラミング入門者からの卒業試験は『ブラックジャック』を開発すべし](https://qiita.com/hirossyi73/items/cf8648c31898216312e5)

JavaScript (というか Nodeで) で解いてみた。

- 90行
- A は 1, 11の両方で評価
- 得点計算、スプリット等の派生ルール等は未実装。

# 概説

## Node ならではの留意点
対話的に作るのが難しくてクソみたいな実装になった。**「ユーザからの入力が来るまで同期的に待つ」というのが、非同期を基本とするJavaScriptでは難しい**。他の言語なら楽なのに。

具体的には、まず、`readline` モジュールを使うことで、標準入力をストリームに変換し、行ごとにロジックを動作させる。この処理が終わるまで次に行かないように無限ループを配置。無限ループは、CPU 使うのはエコじゃないので非同期スリープで実装。無限ループの脱出のために、行が来るごとに起動されるロジックの中でフラグを立てる。

イメージのコードはこう。実際のコードは `playerAction()` を参考のこと。

```js
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()`


# コード

```js
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();
```