こいつのクローンを作ろうと思います笑
あ、ちなみにプログラムとか関係なく、単に遊びたい方は
見た目もそのまんまのゲームを見つけたのでそちらへどうぞ。
(権利的なものは大丈夫なんだろうか?)
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>