質問をすることでしか得られない、回答やアドバイスがある。

15分調べてもわからないことは、質問しよう!

ただいまの
回答率

88.22%

ブラックジャックのプログラムにおいて、MVCを参考に入出力処理を分離したい

解決済

回答 1

投稿

  • 評価
  • クリップ 0
  • VIEW 790

toromou

score 28

オブジェクト指向の勉強として、JavaScriptでトランプを作成しています。  
手始めにブラックジャックを作っていますが、今後別のゲームを作ることも考えて拡張性のある作りにしたいです。
これまで作ったプログラムは以下のようになっています。

const $ = require('jquery');

// カードクラス
class Card {
  constructor(suit, rank = 0) {
    this._suits = ['joker', 'spades', 'hearts', 'diamonds', 'clubs'];

    this.suit = suit;
    this.rank = rank;
    this.isFront = false;
    this.imgSrc = (suit === 0) ? `joker.png` : `${this._suits[suit]}_${this._convertRank(rank)}.png`;
  }

  // カードを表にする
  faceUp() {
    this.isFront = true;
  }

  // カードを裏にする
  faceDown() {
    this.isFront = false;
  }

  // ランクを適切な表示に変更
  _convertRank(rank) {
    const result =
      rank === 1 ? 'A' :
      rank === 11 ? 'J' :
      rank === 12 ? 'Q' :
      rank === 13 ? 'K' :
      rank;
    return result;
  }
}

// 手札クラス
class Hand {
  constructor(cards) {
    this.cards = cards;
  }

  // 受け取ったカードを加える
  pushCard(card) {
    this.cards.push(...card);
  }

  // 指定された枚数のカードを取り出す
  popCard(count = 1) {
    let result = [];
    for (let i = 0; i < count; i++) {
      result.push(this.cards.pop());
    }
    return result;
  }

  // 指定したスート・ランクのカードを取り出す
  selectCard(suit, rank) {
    let result = this.cards.find((elm) => {
      return elm.suit === suit && elm.rank === rank;
    });
    return result;
  }

  // 全てのカードを表にする
  faceUpAll() {
    for (let card of this.cards) {
      card.faceUp();
    }
  }

  // 全てのカードを裏にする
  faceDownAll() {
    for (let card of this.cards) {
      card.faceDown();
    }
  }

  // カードをシャッフルする
  shuffleCard() {
    for (let i = this.cards.length - 1; i > 0; i--) {
      let rand = Math.floor(Math.random() * (i + 1));
      [this.cards[i], this.cards[rand]] = [this.cards[rand], this.cards[i]];
    }
  }
}

// 山札クラス
class Deck extends Hand {
  constructor(joker) {
    let cards = [];

    // ジョーカー以外を生成
    for (let i = 1; i <= 4; i++) {
      for (let j = 1; j <= 13; j++) {
        cards.push(new Card(i, j));
      }
    }

    // 引数で指定した枚数のジョーカーを生成
    for (let i = 0; i < joker; i++) {
      cards.push(new Card(0));
    }

    super(cards);
    this.shuffleCard();
  }
}

// プレイヤークラス
class Player {
  constructor(name, hand) {
    this.name = name;
    this.hand = hand;
    this._points = this._calculatePoints();
  }

  // 合計点数を取得
  get points() {
    return this._calculatePoints();
  }

  // 合計点数を計算
  _calculatePoints() {
    let points = 0;
    let aceCount = 0;
    for (let card of this.hand.cards) {
      points += (() => {
        switch (card.rank) {
          case 11:
          case 12:
          case 13:
            return 10;
          case 1:
            aceCount++;
            return 11;
          default:
            return card.rank;
        }
      })();
    }
    while (points > 21 && aceCount > 0) {
      aceCount--;
      points -= 10;
    }
    return points;
  }
}

// ゲームクラス
class Game {
  constructor(...players) {
    // カード1式
    this.deck = new Deck(1);

    // ディーラー
    this.dealer = new Player('ディーラー', new Hand(this.deck.popCard(2)));
    this.dealer.hand.cards[0].faceUp();

    // プレイヤー
    this.players = [];
    for (let playerName of players) {
      const player = new Player(playerName, new Hand(this.deck.popCard(2)));
      player.hand.faceUpAll();
      this.players.push(player);
    }

    // プレイヤー・ディーラー
    this.everyone = [this.dealer, ...this.players];

    // 順番
    this.nowTurn = 0;

    this._outputScreen();
    this._modifyScreen();
  }

  // ヒット時の処理
  hit(player) {
    player.hand.pushCard(this.deck.popCard());
    player.hand.faceUpAll();
    this._modifyScreen();
  }

  // CPUのターン
  cpuTurn() {
    setTimeout(() => {
      const turn = this.nowTurn;
      if (turn < this.players.length) {
        if (this.players[turn].points < 18) {
          this.hit(this.players[turn]);
        } else {
          this.nowTurn++;
        }
        this.cpuTurn();
      } else {
        this.dealerTurn();
      }
    }, 500);
  }

  // ディーラーのターン
  dealerTurn() {
    setTimeout(() => {
      this.dealer.hand.faceUpAll();
      this._modifyScreen();
      if (this.dealer.points < 17) {
        this.hit(this.dealer);
        this.dealerTurn();
      } else {
        this._modifyResult();
      }
    }, 500);
  }

  // コンテンツを挿入
  _outputScreen() {
    let content = '';
    this.everyone.forEach((player, index) => {
      const className = index === 0 ? 'dealer' : 'seat' + index;
      content += `<dl class="${className}">`;
      content += `<dt class="name">${player.name}</dt>`;
      content += '<dd class="hand"><ul></ul></dd>';
      content += `<dd class="points"></dd>`;
      content += '<dd class="result"></dd>';
      content += '</dl>';
    });
    $('.game_screen').html(content);
    $('.seat1 .points').after(
      '<dd class="button"><button class="hit_button">ヒット</button> ' +
      '<button class="stand_button">スタンド</button></dd>'
    );
  }

  // コンテンツを更新
  _modifyScreen() {
    this.everyone.forEach((player, index) => {
      const className = index === 0 ? '.dealer' : '.seat' + index;
      const $target = $(className);
      let content = '';
      for (let card of player.hand.cards) {
        const imgSrc = card.isFront ? card.imgSrc : 'back_blue.png';
        content += `<li><img src="img/${imgSrc}"></li>`;
      }
      $('.hand ul', $target).html(content);
      $('.points', $target).html(`合計点数: ${player.points}`);

      // バスト時の表示
      if (player.points > 21) {
        $('.points', $target).append(' BUST').css('color', '#f00');
        if (index === 1) $(`.seat1 .button`).remove();
      }
    });
  }

  // 勝敗を判定
  _getResult(player) {
    const dealer = this.dealer.points;
    if (player > 21) return 'Lose...';
    if (dealer > 21) return 'Win!!';
    if (player > dealer) return 'Lose...';
    if (player < dealer) return 'Win!!';
    return 'Draw Game';
  }

  // 結果を表示
  _modifyResult() {
    this.players.forEach((player, index) => {
      $(`.seat${index + 1} .result`).html(this._getResult(player.points));
    });
  }
}

// メイン処理
$(() => {
  const game = new Game('プレイヤー', 'CPU1', 'CPU2');

  // デバッグ用にconsoleに山札の一覧をテーブルで表示
  console.table(game.deck.cards);

  // ヒット
  $('.hit_button').on('click', () => {
    game.hit(game.players[0]);
    if (game.players[0].points > 21) {
      game.cpuTurn();
    }
  });

  // スタンド
  $('.stand_button').on('click', () => {
    game.cpuTurn();
    $(`.seat1 .button`).remove();
  });
});

現状はGameクラスにあらゆる処理を詰め込んでおり、自分でも訳が分からなくなりつつあります。
そこで、MVCなるものを参考にして入出力処理を切り分けようと考えました。
しかし、サイトによって書いてあることが異なっていたり、参考になるコードが見つからなかったりでいまいち理解できていません。
自分なりの解釈ですが、以下のようなイメージで合っているでしょうか。


Model
各データと、それらを処理するメソッドを持つ

View
modelの参照、データを加工し出力するメソッドを持つ

Controller
model・viewの参照、それらのメソッドを順番に実行するメソッドを持つ

// Modelクラス
class Model {
  constructor(data1, data2) {
    this.data1 = data1;
    this.data2 = data2;
  }
  hoge() {
    this.data1++;
  }
  fuga() {
    this.data2--;
  }
}

// Viewクラス
class View {
  constructor(model) {
    this.model = model;
  }
  piyo() {
    console.log(this.model.data1);
    console.log(this.model.data2);
  }
}

// Controllerクラス
class Controller {
  constructor(model, view) {
    this.model = model;
    this.view = view;
  }
  clickBtn() {
    this.model.hoge();
    this.model.fuga();
    this.view.piyo();
  }
}

// メイン処理
$(() => {
  const model = new Model(3, 6);
  const view = new View(model);
  const controller = new Controller(model, view);

  $('.btn').on('click', () => {
    controller.clickBtn();
  });
});

また、他にもオブジェクト指向としておかしな部分があればご教授いただけたら幸いです。

  • 気になる質問をクリップする

    クリップした質問は、後からいつでもマイページで確認できます。

    またクリップした質問に回答があった際、通知やメールを受け取ることができます。

    クリップを取り消します

  • 良い質問の評価を上げる

    以下のような質問は評価を上げましょう

    • 質問内容が明確
    • 自分も答えを知りたい
    • 質問者以外のユーザにも役立つ

    評価が高い質問は、TOPページの「注目」タブのフィードに表示されやすくなります。

    質問の評価を上げたことを取り消します

  • 評価を下げられる数の上限に達しました

    評価を下げることができません

    • 1日5回まで評価を下げられます
    • 1日に1ユーザに対して2回まで評価を下げられます

    質問の評価を下げる

    teratailでは下記のような質問を「具体的に困っていることがない質問」、「サイトポリシーに違反する質問」と定義し、推奨していません。

    • プログラミングに関係のない質問
    • やってほしいことだけを記載した丸投げの質問
    • 問題・課題が含まれていない質問
    • 意図的に内容が抹消された質問
    • 過去に投稿した質問と同じ内容の質問
    • 広告と受け取られるような投稿

    評価が下がると、TOPページの「アクティブ」「注目」タブのフィードに表示されにくくなります。

    質問の評価を下げたことを取り消します

    この機能は開放されていません

    評価を下げる条件を満たしてません

    評価を下げる理由を選択してください

    詳細な説明はこちら

    上記に当てはまらず、質問内容が明確になっていない質問には「情報の追加・修正依頼」機能からコメントをしてください。

    質問の評価を下げる機能の利用条件

    この機能を利用するためには、以下の事項を行う必要があります。

質問への追記・修正、ベストアンサー選択の依頼

  • m.ts10806

    2019/10/11 11:57

    個人的感想なのでこちらに書きますが「オブジェクト指向手始め」としてトランプゲームはかなりハードルが高いように思います。
    設計はきちんとできてるのかな?とか、役割分担はどこまで考えてるのかな?とか、もう少し簡単なものからされたほうが良いと思います。
    ゲームだとしても本当に簡単な数当てゲームとかtrue/falseがハッキリでるものですね。

    キャンセル

  • toromou

    2019/10/11 12:21

    正直、作っていて自分にはまだ早いかもとは思っていましたが、
    一応動くものはできたのでそのまま勢いでやっている状態です。
    確かに、基本的なことを学ぶためにはもっと簡単なもので良いかもしれませんね…
    ご助言いただきありがとうございます。

    キャンセル

  • m.ts10806

    2019/10/11 13:09

    はい。「なんとなく動くものが作れた」だけでは他のものを作るときにまた1からやることになります。
    オブジェクト指向のメリットも加味すると、なるべく依存性の少ない作りにしておいた方が取り扱いやすくなります。

    キャンセル

回答 1

checkベストアンサー

+2

MVC は、表示に関連する部分(V) とロジック(M) を分けるために使うと良いもので、
このプログラムは別に混在してないので、それを当てはめてもあまりメリットが無いような気がします。

オブジェクト指向の勉強として、作ったコードを直していくのであれば、
いったん MVC にはこだわらず、下記あたりの本を読むのが、やりやすいと思います。

  • リファクタリング
  • デザインパターン

投稿

  • 回答の評価を上げる

    以下のような回答は評価を上げましょう

    • 正しい回答
    • わかりやすい回答
    • ためになる回答

    評価が高い回答ほどページの上位に表示されます。

  • 回答の評価を下げる

    下記のような回答は推奨されていません。

    • 間違っている回答
    • 質問の回答になっていない投稿
    • スパムや攻撃的な表現を用いた投稿

    評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。

15分調べてもわからないことは、teratailで質問しよう!

  • ただいまの回答率 88.22%
  • 質問をまとめることで、思考を整理して素早く解決
  • テンプレート機能で、簡単に質問をまとめられる

関連した質問

同じタグがついた質問を見る