Help us understand the problem. What is going on with this article?

【JavaScript】Node.jsでブラックジャック ٩( 'ω' )و

はじめに

以下のような記事を見たので、JavaScriptでブラックジャックを実装してみました。オブジェクト指向っぽく作りました。Node.jsでコマンドラインで遊べます。

プログラミング入門者からの卒業試験は『ブラックジャック』を開発すべし

作るもの

環境

  • MacOS Mojave 10.14.6
  • Node.js v12.4.0
  • Atom

解説について

Node.jsおよびNPMはインストールされている前提で進めます。
また文中に出てくるコードは解説のために省略している部分が多々あります。完全版を見たい方は「全てのコード」またはGithubレポジトリをご覧ください。

Githubレポジトリ: https://github.com/yuta-ike/BlackJack/tree/master

プロジェクトの生成

$ mkdir blackjack
$ cd blackjack/
$ npm init

blackjackフォルダの中にmain.jsを作成します。フォルダ構成は以下のようになります。

blackjack
  ├─ node_modules
  ├─ main.js
  ├─ package.json
  └─ package-lock.json

以降はmain.jsを編集していきます。

種々のクラスの定義

Cardクラス

トランプのカードを表すクラスです。トランプにはスートと数字という二つの要素がありますので、これらをプロパティで表現します。(ブラックジャックではカードのスートには注目しないため数字だけでも良いのですが、描画の際にスートがある方がトランプっぽくなるので追加しておきます。)
加えて、ブラックジャックではJQKは全て10として扱われます。額面の数字と実際の値が異なりますので、実際の値をvalueとしてアクセスできるようにします。

main.js
class Card{
  constructor(suit, number){
    this.suit = suit
    this.number = number
  }

  get value(){
    return this.number >= 11 ? 10 : this.number
  }
}

Deckクラス

デッキを表すクラスです。デッキには、⓵現在デッキに残っている(場に出ていない)カードの集合、⓶カードを一枚プレイヤーorディーラーに引かれる という二つの要素があります。前者はプロパティ、後者はメソッドで表現します。

main.js
const suits = ["Spade","Heart","Diamond","Club"]

class Deck{
  constructor(){ 
    //カードを生成
    this.cards = []
    for(const suit of suits){
      for(let i = 1; i <= 13; i++){
        this.cards.push(new Card(suit, i))
      }
    }
  }

  drawn(num=1){
    //一枚だけデッキから引く関数
    const drawOne = () => {
      //ランダムにカードを取得
      const drawnCard = this.cards[Math.floor(Math.random() * this.cards.length)]
      //デッキから、取得したカードを取り除く
      this.cards = this.cards.filter(card => card !== drawnCard)
      return drawnCard
    }

    if(num === 1) return drawOne()
    return Array(num).fill().map(_ => drawOne())
  }
}

コンストラクタ内でカードを52枚生成しています。

drawnメソッドでは、デッキからカードを引く要素を実現しています。引数には、引くカードの枚数を指定します。1枚の場合は、Cardインスタンス、2枚以上の場合はインスタンスの配列を返します。

メソッド内でdrawOne関数(一枚だけカードを引く関数)を定義し、指定回数分繰り返し呼び出すようになっています。

Array(num).fill()は長さnumで全ての要素がundefinedで埋められた配列を生成しています。これにmapメソッドを適用することで、カードの配列を作っています。

Playerクラス

今回のブラックジャックはスプリットやダブルダウンなどの複雑な部分は実装を見送り、ヒット(追加でカードをもらう)のみを実装します。よってプレイヤークラスには ⓵ハンド(手札)、⓶自分のターンで行動する(ヒットなどのアクションを起こす)という二つの要素を持たせます。⓵はhandsプロパティ、⓶はactメソッドで実装します。

main.js
class Player{
  constructor(id, deck){
    this.id = id
    this.deck = deck
    this.hands = []
  }

  act(){
    while(true){
      if(this.getSum() === 21){
        return
      }
      if(readlineSync.question(`【${this.name}】hitしますか?[y/n] : `) === "y"){
        this.hit()
        const isBurst = this.burstCheck()
        if(isBurst){
          break
        }
      }else{
        break
      }
    }
  }

  hit(num=1){
    if(num === 1){
      this.hands.push(this.deck.drawn())
    }else{
      this.hands = [...this.hands, ...this.deck.drawn(num)]
    }
  }

  getSum(){
    return this.hands.reduce((sum, card) => sum + card.value, 0)
  }

  burstCheck(){
    return this.getSum() > 21
  }
}

handsについてはデッキと同じように、配列で表現します。

actメソッドは以下のアルゴリズムにそって実装します

       +---------------+
         手札の合計が              
+------>       21未満   --no-+
|      +-------+-------+     |
|             yes            |
|              v             |
|     +-------+-----------+  |
|       hitするか             |     
+-hit- プレイヤーに選択させる    |     
      +-------+-----------+  |
               |             |
             no hit          |
               v             |
       +-------+-------+     |
              END       <----+
       +---------------+
               |
               v

一つのメソッドとして実装すると非常に大きくなってしまうため、細かく分割しています。具体的には、手札の合計が21を超えているか(バーストしているか)を判定するburstCheck()、ハンドの合計値を返すgetSum()、デッキからカードを引くhit()を実装しています。
「hitするかプレイヤーに選択させる」部分は、ユーザーに入力を求める処理となりますのでreadline-syncというライブラリを用いて実装しています。readlineSync.question("XXX")で、XXXを標準出力し、ユーザの入力を受けるまで待つことができます。

hitメソッドについては色々考えたのですが、コンストラクタでDeckインスタンスを取得しておき、hitメソッド内でdrawnメソッドを呼び出しています。

コンストラクタで、Deckインスタンスではなくdrawnメソッドを渡す案や、イベントリスナを実装することも考えましたが、前者はthisをうまく扱えなかったこと、後者はコード量が増えることから断念しました。

Gamblerクラス

次にディーラーを表すDealerクラスを考えます。ディーラークラスに必要な要素は、⓵ハンド(手札)、⓶自分のターンで行動する という二つの要素があります。ここでPlayerクラスと、大部分が共通の処理となることが分かります。そこでPlayerクラスとDealerクラスの共通の処理をまとめたスーパークラスの実装を考えます。名前は思いつかなかったのでGambler(ギャンブラー)クラスとしました(笑)

main.js
class Gambler{
  constructor(deck){
    this.deck = deck

    this.hands = []
  }

  hit(num=1){
    if(num === 1){
      this.hands.push(this.deck.drawn())
    }else{
      this.hands = [...this.hands, ...this.deck.drawn(num)]
    }
  }

  getSum(){
    return this.hands.reduce((sum, card) => sum + card.value, 0)
  }

  burstCheck(){
    return this.getSum() > 21
  }
}

hit()getSum()burstCheck()handsdeckはプレイヤーにもディーラーにも共通する要素ですのでGamblerクラスに実装します。一方act()メソッドは、異なる部分か少しあるので今回はそれぞれのクラスに実装しました。

今思うと、プレイヤーとディーラーの行動の違いは、hitを行うか否かの判断は、ユーザー入力を求める or 手札の合計が16以上か という点のみであるので、この部分をメソッドとして切り出し、act()についてはGamblerクラスに実装すべきだったかなと思っています。

Dealerクラス

大部分はGamblerクラスに実装したので、Dealerクラスにはact()のみ実装します。アルゴリズムについても、手札の合計値が16を超えるまでヒットし続けるという単純なものですので説明は割愛します。

main.js
class Dealer extends Gambler{
  act(){
    if(this.getSum() === 21){
      return
    }
    while(this.getSum() <= 16){
      this.hit()
      const isBurst = this.burstCheck()
      if(isBurst){
        break
      }
    }
  }
}

Gameクラス

最後に今までのクラス群を実際にインスタンス化してゲームを進行させる役割を持つGameクラスを実装します。Gameクラスには、ブラックジャックのセットアップを行うstart()と、1ゲームを進行するplay()を実装します。(start()の中身はコンストラクタに実装しても良かったかなと思います。)

プレイヤーは複数人いることを想定して配列で管理しています。

main.js
class Game{
  start(playerNum = 1){
    this.deck = new Deck()

    //ディーラーとプレイヤーの生成
    this.dealer = new Dealer(this.deck)
    this.players = Array(playerNum).fill().map((_, i) => new Player(i, this.deck))
  }

  play(){
    const {players, dealer} = this

    //プレイヤーとディーラーに2枚ヒットさせる(ことで最初に二枚カードを配る)
    players.forEach(player => player.hit(2))
    dealer.hit(2)

    //プレイヤーが順にアクションを起こす
    players.forEach(player => player.act())

    //バーストしたプレイヤーは既に負けが確定するのでendメソッド(後述)を呼び出す
    const decidedPlayers = players.filter(player => player.state === "burst") //stateについても後述
    decidedPlayers.forEach(player => player.end("lose"))

    //プレイヤーが全員バーストした場合はゲーム終了
    if(decidedPlayers.length < players.length) return

    //ディーラーがアクションを起こす
    dealer.act()

    //勝敗判定
    if(dealer.state === "burst"){
      players.filter(player => player.state === "challenge")
             .forEach(player => player.end("win"))
    }else if(dealer.state === "challenge"){
      players.forEach(player => {
        if(21 - dealer.getSum() < 21 - player.getSum()){
          player.end("lose")
        }else if(21 - dealer.getSum() > 21 - player.getSum()){
          player.end("win")
        }else{
          if(dealer.isBJ()){
            player.end("lose")
          }else if(player.isBJ()){
            player.end("win")
          }else{
            player.end("draw")
          }
        }
      })
    }
  }
}

play()は手続き的に実装していきます。
途中に出てくるend()ですがPlayerクラスのメソッドで実装は以下のようになっています。

main.js
class Player{
  ...


  end(result){
    console.log(`【Player${this.id}】You ${result}!`)
  }
}

this.idというのは、Playerクラスのセクションでは触れませんでしたが、プレイヤーを識別する番号です。ロジックを組む上では必要ないのですが、画面にメッセージを表示する場合に、どのプレイヤーに関するメッセージかを表すために用いています。

Gameクラス内に登場するstateは、Gamblerの状態を表すプロパティです。アクションが終了した時点で、そのプレイヤーが ⓵"burst"(バーストしている)、⓶"challenge"(バーストしていない)かを表しています。Gameクラスからは各Gamblerのstateを見て、勝利判定をしています。今まで上げたコードでは、stateプロパティはややこしくなるので省いています。stateの実装については下記の「全てのコード」をご覧ください。

今思えば、これは完全に無駄なプロパティでした。getSum()を呼び出すことで勝利判定は行えますので、わざわざプロパティを設定する必要はありませんでした。

AceCardクラス

ここまでほったらかしてきましたが、ブラックジャックではA(エース)は1または11のどちらかとして扱うことができます。この仕組みを実装していきます。
今回は簡単のために、以下のルールに従います。

  1. Aは最初は11として扱われる
  2. もしバーストした場合は、Aは1として扱われる
  3. 手札に複数のAが含まれている場合、バーストすればそのうちの一枚が1として扱われる。再度バーストした場合は次のAが1として扱われる....

Cardクラスに実装しても良いのですが、ごちゃごちゃしそうなのでCardクラスを継承したAceCardクラスを定義しこちらに実装していきます

main.js
class AceCard extends Card{
  constructor(...arg){
    super(...arg)
    this.lowered = false
  }
  lowerValue(){
    this.lowered = true
  }
  get value(){
    return this.lowered ? 1 : 11
  }
}

lowerdプロパティはAの値が既に1に変化したかを表すフラグです。lowerValue()を呼び出すことで、このカードの値が10から1に変化します。そして、get value()をオーバーライドすることで目的の挙動を実現しました。

呼び出し部分です。「バーストした場合にlowerValue()を呼び出す」というアルゴリズムなので、GamblerクラスのburstCheck()内で呼び出しを行なっています。

main.js
class Gambler{
  ...

  burstCheck(){
    const isBurst = this.getSum() > 21
    if(!isBurst) return false

    //10としてカウントされているエースが無いか調べる
    const ace = this.hands.find(card => card instanceof AceCard && !card.lowered)
    if(ace != null){
      //エースの値を更新し、再度バーストチェック(注)
      ace.lowerValue()
      return this.burstCheck()
    }else{
      return true
    }
  }
}

(注) 現在のルールでは再度バーストチェックを行う必要はないのですが(バーストし得ないから)今後新たなルールを追加した場合などを考えこのようなロジックにしました。

burstCheck()はバーストしているかの判定を返す、言わばクエリに当たるメソッドですので、ここに副作用を伴うlowerValue()の呼び出しを記述するのはあまりよろしくないと思うのですが、横着してしまいました。。。

レンダリング

ここまでゲームロジックについて実装しましたので、次にレンダリング処理を実装していきます。

┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
┃ Dealer:                      ┃
┃ ┏━━━━━┓ ┏━━━━━┓ ┏━━━━━━┓     ┃
┃ ┃ ♦ 3 ┃ ┃ ♠ K ┃ ┃ ♠ 10 ┃     ┃
┃ ┗━━━━━┛ ┗━━━━━┛ ┗━━━━━━┛     ┃ 
┃ 合計値 23                    ┃
┃ Dealerはバーストしました     ┃
┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
┃ Player1:                        ┃
┃ ┏━━━━━┓ ┏━━━━━┓ ┏━━━━━┓         ┃
┃ ┃ ♦ J ┃ ┃ ♠ 2 ┃ ┃ ♣ J ┃         ┃
┃ ┗━━━━━┛ ┗━━━━━┛ ┗━━━━━┛         ┃ 
┃ 合計値 22                       ┃
┃ プレイヤー0はバーストしました   ┃
┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
┃ Player2:                        ┃                                             
┃ ┏━━━━━┓ ┏━━━━━┓ ┏━━━━━┓         ┃
┃ ┃ ♥ J ┃ ┃ ♣ 6 ┃ ┃ ♣ 9 ┃         ┃
┃ ┗━━━━━┛ ┗━━━━━┛ ┗━━━━━┛         ┃ 
┃ 合計値 25                       ┃
┃ プレイヤー1はバーストしました   ┃
┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛

デザインが崩れているのは、Qiitaのフォントが 全角文字幅=半角文字幅 * 2 ではないためです。お使いのエディタ等で等幅フォントを適用していただけるとデザイン崩れは解消されると思います。

renderCard()

まず、以下の部分を描画するメソッドを定義します。グローバル関数として実装します。

┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
┃ Dealer:                      ┃
┃ ┏━━━━━┓ ┏━━━━━┓ ┏━━━━━━┓     ┃
┃ ┃ ♦ 3 ┃ ┃ ♠ K ┃ ┃ ♠ 10 ┃     ┃
┃ ┗━━━━━┛ ┗━━━━━┛ ┗━━━━━━┛     ┃ 
┃ 合計値 23                    ┃
┃ Dealerはバーストしました     ┃
┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
main.js
const renderCard = (name, cards, sum, message=sum === 21 && cards.length === 2 ? "Black Jack!!" : "") => {
  //トランプカード部分の描画文字列の生成
  let upper = "", middle = "", lower = ""
  for(const card of cards){
    upper  += "" + "".repeat(card.mark.length === 4 ? 6 : 5) + ""
    middle += "" + card.mark + ""
    lower  += "" + "".repeat(card.mark.length === 4 ? 6 : 5) + ""
  }

  //大きい長方形の幅を求める
  const cardWidth = Math.max(cards.reduce((sum, card) => sum + (card.mark.length === 4 ? 8 : 7),0) + cards.length + 1, message.length, 30, calcLength(message) + 4)
  const cardText = "" + upper + " ".repeat(cardWidth - upper.length - 1) + "\n" + middle + " ".repeat(cardWidth - upper.length - 1) + "\n" + lower + " ".repeat(cardWidth - upper.length - 1) + ""

  //描画
  process.stdout.write(
`┏${"".repeat(cardWidth)}┓
┃ ${name}:${" ".repeat(cardWidth - name.length - 2)}${cardText}
┃ 合計値 ${sum}${" ".repeat(cardWidth - (""+sum).length - 8)}┃
┃ ${message}${" ".repeat(cardWidth - calcLength(message) - 2)} ┃
┗${"".repeat(cardWidth)}┛`)
}

第一引数にプレイヤー名、第二引数に手札のカードの配列、第三引数に手札の合計値、第四引数にメッセージログを取ります。副作用を持たない純粋な関数として実装しています。
順に説明していきます。

まず、トランプカード部分の描画文字列の生成ですが、

┏━━━━━┓ ┏━━━━━┓ ┏━━━━━━┓
┃ ♦ 3 ┃ ┃ ♠ K ┃ ┃ ♠ 10 ┃
┗━━━━━┛ ┗━━━━━┛ ┗━━━━━━┛

この部分を生成しています。upperが1行目,middleが2行目、lowerが3行目を表しています。
トランプの幅については、文字列の表現が4文字となる10の場合(注)は横幅を8文字に、それ以外(*)は7文字に設定しています。(11から13についてはJQKで表すため一1字)

(注) "♠ 10"は「♠,空白,1,0」で4文字、それ以外は、数字部分が1桁なので3文字(1~9, J, Q, K)

なお、Cardクラスにget mark()を実装しています。トランプの文字列表現("♠ 10"など)を返します。

main.js
class Card{
  ...

  get mark(){
    return suitTable[this.suit] + " " + (this.number === 1 ? "A" : this.number === 11 ? "J" : this.number === 12 ? "Q" : this.number === 13 ? "K" : this.number)
  }
}

次に大きい長方形(名前(DealerやPlayer1など)、トランプカード、合計値、メッセージを含む長方形)の幅を求めます。最低値は30文字とし、メッセージの文字列およびトランプカード全体の横幅の文字列がこれを上回る場合は、その最大値を長方形の幅としています。

残りの描画部分についてはテンプレートリテラル内の変数展開を用いて実装しています。自分でも読み返したくはないので詳細は割愛します。触れる点としては、文字列の長さを取得する際に、全角文字は2文字分としてカウントしてほしいので、以下のような関数を定義しています。

main.js
const calcLength = str => str.length + (str.match(/[^\x01-\x7E]/g) || []).length

str.match(/[^\x01-\x7E]/g).lengthでstrに含まれる全角文字の数を取得できます。これを利用して文字数をカウントしています。

描画処理の呼び出し

ゲームの進捗に応じて描画関数を呼び出します。呼び出す際は以下のように、描画開始座標を設定してからrenderCard関数の呼び出しを行います。

main.js
const height = 8
...

class X{
 ...

 xxx(){
    //actメソッドの中で呼び出されることが多いです
    readline.cursorTo(process.stdout, 0, height * (this.id + 2) + 1);
    process.stdout.write(" ".repeat(process.stdout.columns))
 }
}

readline.cursorTo()はターミナル上でカーソルの位置(ログを書き始める位置)を指定することができるメソッドです。readlineライブラリを用いています。
第一引数にprocess.stdout、第二引数に左から何文字目か、第三引数に上から何行目かを指定できます。
processというのはNode.jsでの組み込みオブジェクトです。

具体的にどこで呼び出しているのかは、下記の「全てのコード」を参照してください。

ゲームの開始

main.js
const game = new Game()
game.start(2)
game.play()

プレイヤーの人数を2人として開始します。

実行

blackjackフォルダに移動し下記コマンドでプログラムを実行できます。

$ node main.js

全てのコード

最後に最終的なコードを貼っておきます。冒頭でも述べましたが、文中に出てくるコードは、説明のために省略している部分が多々ありますので、最終的なコードを見たい方はこちらをご覧ください。

クリックで表示
main.js
const readlineSync = require('readline-sync');
const readline = require('readline');

const suitTable = {
  "Spade":String.fromCodePoint(0x2660),
  "Heart":String.fromCodePoint(0x2665),
  "Diamond":String.fromCodePoint(0x2666),
  "Club":String.fromCodePoint(0x2663),
}

const calcLength = str => str.length + (str.match(/[^\x01-\x7E]/g) || []).length

const height = 8
const renderCard = (name, cards, sum, message=sum === 21 && cards.length === 2 ? "Black Jack!!" : "") => {
  let upper = "", middle = "", lower = ""
  for(const card of cards){
    upper  += "" + "".repeat(card.mark.length === 4 ? 6 : 5) + ""
    middle += "" + card.mark + ""
    lower  += "" + "".repeat(card.mark.length === 4 ? 6 : 5) + ""
  }
  const cardWidth = Math.max(cards.reduce((sum, card) => sum + (card.mark.length === 4 ? 8 : 7),0) + cards.length + 1, 30, calcLength(message) + 4)
  console.log(cardWidth - upper.length - 1)
  const cardText = "" + upper + " ".repeat(cardWidth - upper.length - 1) + "\n" + middle + " ".repeat(cardWidth - upper.length - 1) + "\n" + lower + " ".repeat(cardWidth - upper.length - 1) + ""
  process.stdout.write(
`┏${"".repeat(cardWidth)}┓
┃ ${name}:${" ".repeat(cardWidth - name.length - 2)}${cardText}
┃ 合計値 ${sum}${" ".repeat(cardWidth - (""+sum).length - 8)}┃
┃ ${message}${" ".repeat(cardWidth - calcLength(message) - 2)} ┃
┗${"".repeat(cardWidth)}┛`)
}

class Gambler{
  constructor(deck){
    this.deck = deck

    this.hands = []
    this.state = "none"
  }

  hit(num=1){
    if(num === 1){
      this.hands.push(this.deck.drawn())
    }else{
      this.hands = [...this.hands, ...this.deck.drawn(num)]
    }
  }

  getSum(){
    return this.hands.reduce((sum, card) => sum + card.value, 0)
  }

  burstCheck(){
    const isBurst = this.getSum() > 21
    if(!isBurst) return false
    const ace = this.hands.find(card => card instanceof AceCard && !card.lowered)
    if(ace != null){
      ace.lowerValue()
      return this.burstCheck()
    }else{
      return true
    }
  }

  clear(){
    this.hands = []
  }

  isBJ(){
    return this.getSum() === 21 && this.hands.length === 2
  }
}


class Dealer extends Gambler{
  get name(){return 'Dealer'}

  act(){
    this.state = "challenge"
    if(this.getSum() === 21){
      return
    }
    readline.cursorTo(process.stdout, 0, 0);
    renderCard(this.name, this.hands, this.getSum())
    while(this.getSum() <= 16){
      this.hit()
      const isBurst = this.burstCheck()
      readline.cursorTo(process.stdout, 0, 0);
      renderCard(this.name, this.hands, this.getSum())
      if(isBurst){
        this.state = "burst"
        readline.cursorTo(process.stdout, 0, 0);
        renderCard(this.name, this.hands, this.getSum(), "Dealerはバーストしました")
        break
      }
    }
  }
}

class Player extends Gambler{
  constructor(id, deck, playerNum){
    super(deck)
    this.id = id
    this.playerNum = playerNum
  }

  get name(){return 'Player' + (this.id + 1)}

  act(){
    while(true){
      readline.cursorTo(process.stdout, 0, height * (this.id + 1));
      renderCard(this.name, this.hands, this.getSum())
      if(this.getSum() === 21){
        this.state = "challenge"
        return
      }
      readline.cursorTo(process.stdout, 0, height * (this.playerNum + 1) + 1);
      if(readlineSync.question(`【${this.name}】hitしますか?[y/n] : `) === "y"){
        this.hit()
        const isBurst = this.burstCheck()
        if(isBurst){
          this.state = "burst"
          readline.cursorTo(process.stdout, 0, height * (this.id + 1));
          renderCard(this.name, this.hands, this.getSum(), `プレイヤー${this.id}はバーストしました`)
          break
        }
      }else{
        this.state = "challenge"
        break
      }
    }
    readline.cursorTo(process.stdout, 0, height * (this.id + 2) + 1);
    process.stdout.write(" ".repeat(process.stdout.columns))
  }

  end(result){
    readline.cursorTo(process.stdout, 0, height * (this.playerNum + 1) + 1 + this.id);
    process.stdout.write(`【${this.name}】You ${result}!`)
  }
}

const suits = ["Spade","Heart","Diamond","Club"]

class Deck{
  constructor(){
    this.cards = []
    for(const suit of suits){
      this.cards.push(new AceCard(suit, 1))
      for(let i = 2; i <= 13; i++){
        this.cards.push(new Card(suit, i))
      }
    }
  }

  drawn(num=1){
    const drawOne = () => {
      const drawnCard = this.cards[Math.floor(Math.random() * this.cards.length)]
      this.cards = this.cards.filter(card => card !== drawnCard)
      return drawnCard
    }

    if(num === 1) return drawOne()
    return Array(num).fill().map(_ => drawOne())
  }
}

class Card{
  constructor(suit, number){
    this.suit = suit
    this.number = number
  }

  get value(){
    return this.number >= 11 ? 10 : this.number
  }

  get mark(){
    return suitTable[this.suit] + " " + (this.number === 1 ? "A" : this.number === 11 ? "J" : this.number === 12 ? "Q" : this.number === 13 ? "K" : this.number)
  }
}

class AceCard extends Card{
  constructor(...arg){
    super(...arg)
    this.lowered = false
  }
  lowerValue(){
    this.lowered = true
  }
  get value(){
    return this.lowered ? 1 : 11
  }
}

class SecretCard extends Card{
  get size(){return 1}
  get mark(){return " ? "}
}

class Game{
  start(playerNum = 1){
    this.deck = new Deck()

    this.dealer = new Dealer(this.deck)
    this.players = Array(playerNum).fill().map((_, i) => new Player(i, this.deck, playerNum))

    process.stdout.write("\n".repeat(process.stdout.rows))
    readline.cursorTo(process.stdout, 0, 0);

  }

  play(){
    const {players, dealer} = this
    players.forEach(player => player.hit(2))
    dealer.hit(2)
    renderCard('Dealer', [dealer.hands[0], new SecretCard()], "?")

    players.forEach(player => player.act())

    const decidedPlayers = players.filter(player => player.state === "burst")
    decidedPlayers.forEach(player => player.end("lose"))

    if(decidedPlayers.length == players.length) return

    dealer.act()
    if(dealer.state === "burst"){
      players.filter(player => player.state === "challenge")
             .forEach(player => player.end("win"))
    }else if(dealer.state === "challenge"){
      players.forEach(player => {
        if(21 - dealer.getSum() < 21 - player.getSum()){
          player.end("lose")
        }else if(21 - dealer.getSum() > 21 - player.getSum()){
          player.end("win")
        }else{
          if(dealer.isBJ()){
            player.end("lose")
          }else if(player.isBJ()){
            player.end("win")
          }else{
            player.end("draw")
          }
        }
      })
    }
    readline.cursorTo(process.stdout, 0, process.stdout.rows - 1);
  }
}

const game = new Game()
game.start(2)
game.play()

Why do not you register as a user and use Qiita more conveniently?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away