0
0

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

Last updated at Posted at 2024-04-29

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

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

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

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

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の整数値でランダム
}

使い方はこんな感じ。

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で作るゲーム【ジャンケンマンフィーバー】表示処理編
続きの記事を書きました。

<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>
0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0