javascriptで作るゲーム【ジャンケンマンフィーバー】ロジック編

https://ja.wikipedia.org/wiki/%E3%82%B8%E3%83%A3%E3%83%B3%E3%82%B1%E3%83%B3%E3%83%9E%E3%83%B3_(%E3%82%A2%E3%83%BC%E3%82%B1%E3%83%BC%E3%83%89%E3%82%B2%E3%83%BC%E3%83%A0)

こいつのクローンを作ろうと思います笑

あ、ちなみにプログラムとか関係なく、単に遊びたい方は
見た目もそのまんまのゲームを見つけたのでそちらへどうぞ。
（権利的なものは大丈夫なんだろうか？）
https://hitoikigame.com/blog-entry-8538.html

# STEP1: ロジックを完成させる

MVCじゃないですけど、見た目は完全無視して、Controller的な部分から作成。（Modelもか？）
コンソール上で対話すればそれっぽく動くところまでを完成させます。
早速ですけど、できあがりはこちら。

```javascript
class JankenFever {
	#status; // プライベートプロパティ、ゲームの状態
	constructor() {
		this.STS = { STANDBY: 0, PLAYING: 1, SPINING: 2 }; // 状態定義
		this.RSP = [ 'R', 'S', 'P' ]; // Rock, Scissors, Paper
		this.ROULETTE = [1, 2, 4, 7, 2, 4, 1, 2, 7, 4, 2, 20]; // ルーレットの数字の並び
		this.RESULT = { // じゃんけんの結果。nextは結果ごとの次の状態。callbackは表示処理用
			WIN:  { val:'WIN',  next: this.STS.SPINING, callback: this.onwin  },
			LOSE: { val:'LOSE', next: this.STS.STANDBY, callback: this.onlose },
			DRAW: { val:'DRAW', next: this.STS.PLAYING, callback: this.ondraw },
		};
		this.JUDGE = { // じゃんけんの結果を判定するデータ。組み合わせ少ないから全部定義した。
			'RR': this.RESULT.DRAW,	'SS': this.RESULT.DRAW, 'PP': this.RESULT.DRAW,
			'RS': this.RESULT.WIN, 	'SP': this.RESULT.WIN, 	'PR': this.RESULT.WIN,
			'RP': this.RESULT.LOSE,	'SR': this.RESULT.LOSE, 'PS': this.RESULT.LOSE,		
		};
		this.status = this.STS.STANDBY; // 初期状態は「待機」=メダル投入前
		this.medal = 1000; // ゲーム機側のメダル枚数（なくても動くけど気持ち的に、ね）
		this.player = { medal: 10 }; // プレイヤー。メダル10枚スタート。
		this.onstart = null; // 表示用のコールバック関数。今のところ未設定でOK
	}
	set status(sts) { // statusの雪駄
		this.#status = sts;
		if (sts === this.STS.SPINING) this.spin(); // SPINING状態に入ったらspin()を実行
	}
	get status() { return this.#status;	} // statusの下駄
	set onwin(fn)  { this.RESULT.WIN.callback = fn; } // 勝った時の表示用callback
	set onlose(fn) { this.RESULT.LOSE.callback = fn; } // 負けた時の表示用callback
	set ondraw(fn) { this.RESULT.DRAW.callback = fn; } // 引き分けの時の表示用callback
	start() { // メダルを投入してゲームを開始する（リターン値はデバッグ用）
		if (this.status === this.STS.STANDBY && this.insertMedal(this.player)) {
			this.status = this.STS.PLAYING; // 状態 → 「じゃーんけーん・・・」（ボタン押し待ち）
			this.onstart ? this.onstart() : null; // callbackを実行（表示の更新）
			return { result: true, medal: this.medal, pmedal: this.player.medal };
		} else { return { reslut: false, status: this.status, medal: this.medal, pmedal: this.player.medal }; }
	}
	play(yourHand) { // グー、チョキ、パーを出す（リターン値はデバッグ用）
		if (this.status == this.STS.PLAYING && this.RSP.indexOf(yourHand) > -1) {
			const myHand = this.RSP[this.rnd(this.RSP.length)]; // 機械が何を出すか決める
			const result = this.judge(myHand, yourHand); // 勝敗を判定する
			result.callback ? result.callback() : null; // callbackを実行（表示の更新）
			this.status = result.next; // 状態 → 勝ち・負け・引き分けで次の状態が決まる
			return { result: result.val, medal: this.medal, pmedal: this.player.medal };
		} else { return { result: false, status: this.status, yourHand }; }
	}
	spin() { // ルーレットを回す（あとで手を入れる予定）
		if (this.status == this.STS.SPINING) {
			const medal = this.ROULETTE[this.rnd(this.ROULETTE.length)]; // 回すというか瞬時に止まるw
			this.medal -= medal; // 機械のメダルを減らして、
			this.player.medal += medal; // プレイヤーのメダルを増やす
			this.status = this.STS.STANDBY; // 状態 →　メダルの投入待ち
			return { result: true, medal: this.medal, pmedal: this.player.medal };
		} else { return { result: false, status: this.status }; }
	}
	insertMedal(player) { // メダルを投入する処理
		if (player.medal > 0 && this.medal > Math.max.apply(null,this.ROULETTE)) { // プレイヤーがメダルを持ってない、あるいは、機械に払い出せるメダルがない時は、メダル投入はエラー
			player.medal--; // プレイヤーのメダルを減らして、
			this.medal++; // 機械のメダルを増やす
			return true; // 正常終了
		}
		return false; // エラー
	}
	judge(myHand, yourHand) { return this.JUDGE[myHand+yourHand]; } // 勝敗の判定
	rnd(m) { return Math.floor(Math.random()*m); } // 0〜m-1の整数値でランダム
}
```

使い方はこんな感じ。

```javascript
const game = new JankenFever();
game.start(); // メダルを投入してゲームを開始する
let r = game.play('P'); // じゃんけんを出す。Rock, Scissors, Paperの頭文字1文字を指定
// 勝ち、負け、引き分けのいずれかになる。勝ちの場合は勝手にルーレット回り終わるところまで動く
// r.result === 'DRAW' ならもう一度play()を呼ぶ、'WIN'か'LOSE'ならstart()からやり直す。
```

## ポイント

- なんとなくclassにしてみた。
- getter/setterを使うにあたって、プライベートプロパティを使ってみた。
- enum的なものとか定数とかはstaticにしたり別の書き方あると思うけど、そのへんは省略。
- なるべく処理は書かないポリシー。定義を変更すればカスタマイズできるように。
- あとで(次回?)説明するけど、表示処理はcallbackにすると表示側で制御しなくて済む。

## 雑談

- game.start()とgame.play()を100回くらい繰り返すと、スタート10枚なのに150枚前後まで増えちゃう。
- このままじゃゲームにならないし、メーカーが儲からない（サンワイズという会社はもう無いらしい・・悲）
- ChatGPTに聞いてみたら100回目の期待値は約276.67枚らしい。（本当かどうかは分からん）
```
確率の計算をしてほしい。
AさんとBさんがいる。Aさんは最初にメダルを10枚持っている。
Bさんは最初にメダルを1000枚持っている。
じゃんけんゲーム1回の流れは以下のとおり。
- AさんはBさんにメダルを1枚渡す。
- AさんとBさんが、勝敗がつくまでじゃんけんをする。
- Bさんが勝利した場合はゲーム終了。
- Aさんが勝利した場合は、ランダムに以下のいずれかの枚数をBさんがAさんに渡す。カッコ内は確率を示す。
    - 1枚(2/12)、2枚(4/12)、4枚(3/12)、7枚(2/12)、20枚(1/12)
さて、じゃんけんゲームを100回やった場合に、Aさんのメダル枚数の期待値は何枚になる？
```
# STEP2: 次回、見た目を整える

予告編、ということで、ブラウザでも見れるようにソース掲載しておきます。
この「超絶最低限の見た目」を次回、リッチにしていこうじゃないか。
（HTMLフラグメントですがこのままでもブラウザで動きます）

追記:
https://qiita.com/tri-comma/items/be169c9163d637d82af3
　「 `javascriptで作るゲーム【ジャンケンマンフィーバー】表示処理編` 」
続きの記事を書きました。

```html
<ul>
	<li><button class="insertMedal">メダル投入</button></li>
	<li><button class="hand" disabled value="R">グー</button></li>
	<li><button class="hand" disabled value="S">チョキ</button></li>
	<li><button class="hand" disabled value="P">パー</button></li>
</ul>
<p class="msg">ぺぺぺぺぺ</p>
<p>メダル<span id="myMedal">10</span>枚</p>
<script>
class View {
	constructor(game) {
		this.game = game;
		this.$('.insertMedal').onclick = ()=>this.game.start();
		this.$('.hand').onclick = (e)=>this.game.play(e.currentTarget.value);
		this.game.onstart = ()=>{
			this.$('.insertMedal').disabled = true;
			this.$('.hand').disabled = false;
			this.$('.msg').innerText = 'じゃん、けん、・・・';
			this.$('#myMedal').innerText = this.game.player.medal;
		};
		this.game.ondraw = ()=>this.$('.msg').innerText = 'あーいこーで・・・';
		this.game.onwin = ()=>{
			this.$('.msg').innerText = 'フィーバー！！';
			this.$('.insertMedal').disabled = false;
			this.$('.hand').disabled = true;
			this.$('#myMedal').innerText = this.game.player.medal;
		};
		this.game.onlose = ()=>{
			this.$('.msg').innerText = 'ずこぉorz';
			this.$('.insertMedal').disabled = false;
			this.$('.hand').disabled = true;
			this.$('#myMedal').innerText = this.game.player.medal;
		};
	}
	$(sel) { return new CustomDom(document.querySelectorAll(sel)); }
}
class CustomDom {
	constructor(dom) { this.mydom = dom; }
	set onclick(fn) { this.mydom.forEach( element => element.onclick = fn ); }
	set disabled(flg) { this.mydom.forEach( element => element.disabled = flg ); }
	set innerText(txt) { this.mydom.forEach( element => element.innerText = txt ); }
}
class JankenFever {
	#status;
	constructor() {
		this.STS = { STANDBY: 0, PLAYING: 1, SPINING: 2 };
		this.RSP = [ 'R', 'S', 'P' ];
		this.ROULETTE = [1, 2, 4, 7, 2, 4, 1, 2, 7, 4, 2, 20];
		this.RESULT = {
			WIN:  { val:'WIN',  next: this.STS.SPINING, callback: this.onwin  },
			LOSE: { val:'LOSE', next: this.STS.STANDBY, callback: this.onlose },
			DRAW: { val:'DRAW', next: this.STS.PLAYING, callback: this.ondraw },
		};
		this.JUDGE = {
			'RR': this.RESULT.DRAW,	'SS': this.RESULT.DRAW, 'PP': this.RESULT.DRAW,
			'RS': this.RESULT.WIN, 	'SP': this.RESULT.WIN, 	'PR': this.RESULT.WIN,
			'RP': this.RESULT.LOSE,	'SR': this.RESULT.LOSE, 'PS': this.RESULT.LOSE,		
		};
		this.status = this.STS.STANDBY;
		this.medal = 1000;
		this.player = { medal: 10 };
		this.onstart = null;
	}
	set status(sts) {
		this.#status = sts;
		if (sts === this.STS.SPINING) this.spin();
	}
	get status() { return this.#status;	}
	set onwin(fn)  { this.RESULT.WIN.callback = fn; }
	set onlose(fn) { this.RESULT.LOSE.callback = fn; }
	set ondraw(fn) { this.RESULT.DRAW.callback = fn; }
	start() {
		if (this.status === this.STS.STANDBY && this.insertMedal(this.player)) {
			this.status = this.STS.PLAYING;
			this.onstart ? this.onstart() : null;
			return { result: true, medal: this.medal, pmedal: this.player.medal };
		} else { return { reslut: false, status: this.status, medal: this.medal, pmedal: this.player.medal }; }
	}
	play(yourHand) {
		if (this.status == this.STS.PLAYING && this.RSP.indexOf(yourHand) > -1) {
			const myHand = this.RSP[this.rnd(this.rnd(this.RSP.length))];
			const result = this.judge(myHand, yourHand);
			result.callback ? result.callback() : null;
			this.status = result.next;
			return { result: result.val, medal: this.medal, pmedal: this.player.medal };
		} else { return { result: false, status: this.status, yourHand }; }
	}
	spin() {
		if (this.status == this.STS.SPINING) {
			const medal = this.ROULETTE[this.rnd(this.ROULETTE.length)];
			this.medal -= medal;
			this.player.medal += medal;
			this.status = this.STS.STANDBY;
			return { result: true, medal: this.medal, pmedal: this.player.medal };
		} else { return { result: false, status: this.status }; }
	}
	insertMedal(player) {
		if (player.medal > 0 && this.medal > Math.max.apply(null,this.ROULETTE)) {
			player.medal--;
			this.medal++;
			return true;
		}
		return false;
	}
	judge(myHand, yourHand) { return this.JUDGE[myHand+yourHand]; }
	rnd(m) { return Math.floor(Math.random()*m); }
}

const game = new JankenFever();
const view = new View(game);

</script>
```