きっかけ
プログラミング入門者からの卒業試験は『ブラックジャック』を開発すべし
「プログラミング入門者からの卒業試験は『ブラックジャック』を開発すべし」という記事に感化されて、Javaでブラックジャックを作成してみた。
意外と難しい…。設計スキルの低さが顕著に出ますね。
実装の難易度はちょうどよかったです。実装時間は3時間くらいかな?
きれいにしていきたいのでぜひご指摘お願いします。
ソース
ソースコードはgithub
にアップしました。
https://github.com/akiyu33333/blackjack
開発環境
- windows10
- Java8
- IntelliJ IDEA
- Lombok 1.18.8
ソース全文(06/22時点)
仕様は Qiitaの元記事 or githubのREADMEにあるので参照ください。
※指摘を受けてリファクタリングしました。
さらにリファクタリング & Aの点数計算を実装しました。
06/22 追記
@watashi_da さんの指摘を受けてさらに修正しました。
CPUフラグを持たせればよいとアドバイスをいただきまして、リファクタリングの結果、最終的にUserとDealerでクラスを分けることにしました。
Main.java
import blackjackgame.BlackJackGame;
public class Main {
public static void main(String[] args) {
new BlackJackGame().start();
}
}
BlackJackGame.java
クラス名とパッケージ名を変更した。
package blackjackgame;
import card.Deck;
import player.Dealer;
import player.AbstractPlayer;
import player.User;
public class BlackJackGame {
/**
* ゲーム開始
*/
public void start() {
System.out.println("★☆★☆★☆★☆★☆★☆ ブラックジャックにようこそ! ★☆★☆★☆★☆★☆★☆\n");
System.out.println("ゲームを開始します。\n");
Deck deck = new Deck();
AbstractPlayer user = new User("あなた");
AbstractPlayer dealer = new Dealer("ディーラー");
user.initCardList(deck);
dealer.initCardList(deck);
user.drawCard(deck);
if(!user.isBust()) dealer.drawCard(deck);
printGameResult(user, dealer);
System.out.println("\nブラックジャック終了!また遊んでね★");
}
/**
* ゲームの結果を表示
* @param player1 プレイヤー1
* @param player2 プレイヤー2
*/
private void printGameResult(AbstractPlayer player1, AbstractPlayer player2) {
if (player1.calcScore() == player2.calcScore()) {
System.out.println("引き分けです。");
return;
}
AbstractPlayer winner = !player1.isBust() && (player2.isBust() || player1.calcScore() > player2.calcScore())
? player1
: player2;
System.out.println( winner.getName() + "の勝ちです!" );
}
}
Player.java → AbstractPlayer.java
lombok
は必要なところだけ生成するように意識した。
最初は抽象クラスにしていたのだが、いらなくなったので具象クラスにした。
interface作って実装した方がいいのかな。
getPoint()
calcScore()
のラムダを書けたことに個人的に成長を感じた(笑)
getPoint
の名前をcalcScore
に変えた。
リーダブルコードの以下を失念していた。
多くのプログラマは、getで始まるメソッドはメンバの値を返すだけの「軽量アクセサ」であるという規約に慣れ親しんでいる。
この規約を守らなければ、誤解を招く可能性がある。
ここは計算処理が入るのでgetXXX
ではだめですね。
Aの計算ロジックを追加。
A以外を計算し、その後Aの枚数より計算して加算する方式にした。
06/22 追記
抽象クラスにしたため、名前も合わせて変更。
package player;
import card.Card;
import card.Deck;
import lombok.Getter;
import lombok.Setter;
import java.util.ArrayList;
import java.util.List;
public abstract class AbstractPlayer {
protected static final int BUST_POINT = 21;
@Getter
private final String name;
private List<Card> cardList = new ArrayList<>();
@Getter
@Setter
private boolean isBust = false;
public AbstractPlayer(String name) {
this.name = name;
}
private void addCardList(Card card){
cardList.add(card);
}
public int calcScore(){
int score = cardList.stream().filter(card -> card.getPoint() > 1 ).mapToInt(card -> card.getPoint()).sum();
int aceCardCount = (int) cardList.stream().filter(card -> card.getPoint() == 1 ).count();
if (aceCardCount == 0) return score;
int borderScore = 11 - aceCardCount;
return score > borderScore ? score + aceCardCount : score + 10 + aceCardCount ;
}
public void draw(Deck deck) {
draw(deck,false);
}
/**
* 山札からカードを引く
* @param deck 山札
* @param isHidden 引いたカードをを隠すか
*/
public void draw(Deck deck, boolean isHidden) {
Card card = deck.draw();
addCardList(card);
if (calcScore() > BUST_POINT) setBust(true);
String msg = isHidden
? this.name + "の引いたカードはわかりません。"
: this.name + "の引いたカードは" + card.toString() + "です。";
System.out.println( msg );
}
/**
* 初期手札の作成
* @param deck 山札
*/
public abstract void initCardList(Deck deck);
/**
* 山札からカードを引く
* @param deck 山札
*/
public abstract void drawCard(Deck deck);
}
User.java
06/22 追記
プレイヤーとCPUを区別するため、新たに作成。
package player;
import card.Deck;
import java.util.Objects;
import java.util.Scanner;
public class User extends AbstractPlayer {
public User(String name) {
super(name);
}
@Override
public void initCardList(Deck deck) {
draw(deck);
draw(deck);
}
@Override
public void drawCard(Deck deck) {
System.out.println( getName() + "の現在の得点は" + calcScore() + "点です。\n");
try (Scanner sc = new Scanner(System.in)) {
String line = null;
while (!isBust() && !Objects.equals(line, "N")) {
System.out.println("カードを引きますか?引く場合はYを引かない場合はNを入力してください。");
line = sc.nextLine();
if (Objects.equals(line, "Y")) {
draw(deck);
System.out.println( getName() + "の現在の得点は" + calcScore() + "点です。\n");
} else if (!Objects.equals(line, "N")) {
System.out.println("Y/N以外が入力されました。");
}
}
}
}
}
Dealer.java
06/22 追記
プレイヤーとCPUを区別するため、新たに作成。
package player;
import card.Deck;
public class Dealer extends AbstractPlayer {
public Dealer(String name) {
super(name);
}
@Override
public void initCardList(Deck deck) {
draw(deck);
draw(deck, true);
}
@Override
public void drawCard(Deck deck) {
System.out.println( getName() + "の現在の得点は" + calcScore() + "点です。\n");
while (calcScore() < 17){
draw(deck);
System.out.println( getName() + "の現在の得点は" + calcScore() + "点です。\n");
}
}
}
Card.java
フィールドをfinal
にした。
switch
と三項演算子がなんかダサい。
package card;
import lombok.AllArgsConstructor;
@AllArgsConstructor
public class Card {
private final Suit suit;
private final int rank;
private String toDisplayValue() {
switch (this.rank) {
case 1:
return "A";
case 11:
return "J";
case 12:
return "Q";
case 13:
return "K";
default:
return String.valueOf(this.rank);
}
}
public int getPoint() {
return this.rank > 10 ? 10 : this.rank;
}
@Override
public String toString() {
return this.suit.getMark() + "の" + this.toDisplayValue();
}
}
Deck.java
イニシャライズメソッドを使ってみた。
ここのラムダも気分がよかった(笑)
new ArrayList<>()
を使わずに ラムダの戻り値から bill
にそのまま代入したいんだけど書き方がわからなかった。
ラムダで書けたので修正。
言語は違うが、この記事を読んでなんでもかんでもforeachを使うべきではないと思った。
JavaScript で forEach を使うのは最終手段
実際JavaScriptではないので記事の内容がそのまま使えるわけではないが、考え方に感銘を受けた。
Collections.shuffle
これも初めて知った。便利なメソッドがあるんだね。
package card;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
public class Deck {
private List<Card> bill;
{
bill = Arrays.stream(Suit.values()).flatMap(s -> IntStream.rangeClosed(1,13).mapToObj(i -> new Card(s,i))).collect(Collectors.toList());
Collections.shuffle(bill);
}
public Card draw(){
Card card = bill.get(0);
bill.remove(0);
return card;
}
}
Suit.java
大好きなenum
。
enum
でもlombok
を使えることを失念していたので修正。
package card;
import lombok.AllArgsConstructor;
import lombok.Getter;
@AllArgsConstructor
public enum Suit {
SPADE("スペード"),
HEART("ハート"),
DIAMOND("ダイヤ"),
CLUB("クラブ");
@Getter
private final String mark;
}
まとめ
楽しかった。
IntelliJってJavadoc作るショートカットないんだね。
Aを1と11で振り分ける対応と Aはできたので、複数プレイヤー対応とかしていきたい。