javascriptで作るゲーム【ジャンケンマンフィーバー】表示処理編

前回まではこちらを参照してください。
https://qiita.com/tri-comma/items/e9790b420d9786f91149

表示は何で実装しようか考えた結果、canvasにしました。
まだメダル枚数表示とか音とか、実装すべきものは残ってますが、いったん出来たところまでで記事にします。
最後に動くソース一式を掲載しますが、まずはhtmlからどうぞ。

# HTML部分

シンプル。

```html
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>ジャンケンマンフィーバー2024</title>
    <style>
body {
	margin: 0;
	overflow: hidden;
}
#canvas {
	margin: auto;
	width: 100vh;
	height: 100vh;
}
#base {
	display: none;
}
    </style>
</head>
<body>
	<canvas id="canvas"></canvas>
	<img id="base" src="rps.png" />
	<script>
// ここに処理を書きます
    </script>
</body>

</html>
```

描画領域のcanvasタグと、描画素材のimgタグがあるだけです。
ちなみに画像はこんな感じ。
（DALL-E3で作成したものをレタッチ。何気にこれが一番時間かかったんじゃないか？）

![rps.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/127776/26d61dc8-2d0c-ccaa-52c8-926c0a31d4b1.png)


# ゲームロジック部分

前回から少し変更があるので再掲します。

```javascript
class JankenFever {
	#status;
	constructor(ms = 100) {
		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;
		this.onspin = null;
		this.onstop = null;
		this.interval = ms;
		this.ridx = 0;
	}
	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);
			this.status = result.next;
			result.callback ? result.callback() : null;
			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) {
			this.ridx = this.rnd(this.ROULETTE.length);
			this.scnt = 0;
			this.riid = setInterval(()=>{
				this.onspin ? this.onspin(this.ridx, this.ROULETTE) : null;
				this.ridx = (this.ridx + 1) % this.ROULETTE.length;
				if (this.scnt++ > 31) {
					clearInterval(this.riid);
					this.stop();
				}
			}, this.interval);
			return { result: true, medal: this.medal, pmedal: this.player.medal };
		} else { return { result: false, status: this.status }; }
	}
	stop() {
		if (this.status == this.STS.SPINING) {
			const medal = this.ROULETTE[this.ridx];
			this.medal -= medal;
			this.player.medal += medal;
			this.status = this.STS.STANDBY;
			this.onstop ? this.onstop() : null;
			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); }
}
```

## 変更点

- spinメソッドを変更：　onspinイベントを一定周期で発生させる、規定回数が来たらstopメソッドを呼び出す
- stopメソッドを追加： メダルの払い出しとonstopイベントの呼び出し、ステータスをスタンバイに変更
- コンストラクタにonspinの発生周期（ミリ秒）を指定できるように変更

# 表示制御部分

ちょっと長いですけど、まずはソースから。
このあと簡単な解説もあります。

```javascript
class JFView {
	constructor(cid, iid) {
		this.game = new JankenFever(100);
		this.game.onstart = ()=>this.drawComThinking();
		this.game.onwin = ()=>{
			clearInterval(this.plyItvID);
			this.canInsertMedal = true;
			this.blinkResult('win');
		};
		this.game.onlose = ()=>{
			clearInterval(this.plyItvID);
			this.canInsertMedal = true;
			this.blinkResult('lose');
			this.plyItvID = setTimeout(()=>this.drawDemo(), 4000);
		};
		this.game.ondraw = ()=>{
			clearInterval(this.plyItvID);
			this.blinkResult('draw');
			this.plyItvID = setTimeout(()=>this.drawComThinking(false), 1500);
		};
		this.game.onspin = (ri, rm) => this.drawRoulette(ri, rm);
		this.game.onstop = () => this.plyItvID = setTimeout(()=>this.drawDemo(), 3000);
		this.canvas = document.getElementById(cid);
		this.ctx = this.canvas.getContext('2d');
		this.image = document.getElementById(iid);
		this.canvas.width = 1024 * (window.innerWidth / window.innerHeight);
		this.canvas.height = 1024;
		this.canvas.style.width = window.innerWidth + 'px';
		this.canvas.style.height = window.innerHeight + 'px';
		this.wmgn = Math.floor((this.canvas.width - 1024) / 2);
		this.image.addEventListener('load', e=>this.drawInit());
		this.canInsertMedal = true;
		this.plyItvID;
		this.resItvID;
		this.myHand = '';
		this.cx = (canvas.width - 1024) / 2 + 536;
		this.cy = 411;
		this.r = 130;
		this.degs = [-76,-47,-15,17,49,77,102,131,163,-165,-133,-101];
		this.fs = 28;
		this.ctx.font = `bold ${this.fs}px Impact`;
		this.ctx.lineWidth = 2.5;
		this.canvas.addEventListener('click', (e) => {
			const cvsx = Math.floor(1024 * (e.x / window.innerHeight) - this.wmgn);
			const cvsy = Math.floor(1024 * (e.y / window.innerHeight));
			if (cvsx>440 && cvsx<615 && cvsy>705 && cvsy<925 && this.canInsertMedal) {
				clearInterval(this.plyItvID);
				this.game.start();
			}
			if (cvsx>355 && cvsx<441 && cvsy>586 && cvsy<632) this.onpress('R');
			if (cvsx>487 && cvsx<573 && cvsy>586 && cvsy<632) this.onpress('S');
			if (cvsx>621 && cvsx<707 && cvsy>586 && cvsy<632) this.onpress('P');
		});
	}
	drawInit() {
		const { ctx, image, wmgn } = this;
		ctx.drawImage(image, 0, 0, 1, 1024, 0, 0, wmgn + 1, 1024);
		ctx.drawImage(image, 1022, 0, 1, 1024, wmgn + 1023, 0, wmgn + 1, 1024);
		ctx.drawImage(image, 0, 0, 1024, 1024, wmgn, 0, 1024, 1024);
		this.drawDemo();
	}
	drawDemo() {
		this.drawComHand(0);
		this.plyItvID = setInterval((()=>{
			let rsp = 0;
			return () => this.drawComHand(rsp = (rsp + 1) % 3);
		})(), 1000);	
	}
	drawComHand(rsp) {
		const { ctx, image, wmgn, game } = this;
		const rsp_xy = [ {x:1024,y:0}, {x:1024,y:170}, {x:1024,y:340} ][rsp];
		ctx.drawImage(image, rsp_xy.x, rsp_xy.y, 150, 170, wmgn + 448, 317, 150, 170);
		this.drawRoulette(-1, game.ROULETTE);
	}
	drawRoulette(rt = -1, rtmst) {
		const { degs, ctx, cx, cy, r, fs } = this;
		let i = 0;
		rtmst.forEach((num)=>{
			const [numStr, rad] = [num + '', Math.PI / 180 * degs[i]];
			ctx.strokeStyle = rt===i ? '#FF0' : ['#C22','#C22'][i%2];
			ctx.fillStyle = rt===i ? '#F00' : ['#FFF','#FFC'][i%2];
			const dx = Math.floor(cx + r * Math.cos(rad) - (fs * numStr.length) / 2);
			const dy = Math.floor(cy + r * Math.sin(rad) * 185/210);
			ctx.strokeText(numStr, dx, dy);
			ctx.fillText(numStr, dx, dy);
			i++;
		});
	}
	onpress(rsp) {
		this.drawButton(rsp);
		if (!this.canInsertMedal) this.game.play(this.myHand = rsp);
	}
	drawButton(rsp) {
		const { ctx, image, wmgn } = this;
		const xy = {
			R: { px:1024, py:614, bx:355, by:586 },
			S: { px:1024, py:660, bx:487, by:586 },
			P: { px:1024, py:706, bx:621, by:586 }
		}[rsp];
		ctx.drawImage(image, xy.px, xy.py, 86, 46, wmgn + xy.bx, xy.by, 86, 46);
		setTimeout( () => ctx.drawImage(image, xy.bx, xy.by, 86, 46, wmgn + xy.bx, xy.by, 86, 46), 150);
	}
	drawComThinking(flush = true) {
		this.canInsertMedal = false;
		flush ? this.blinkOff() : null;
		let rsp = 0;
		this.plyItvID = setInterval(() => {
			rsp = ([0,1,2].filter(i=>i!==rsp))[Math.round(Math.random())];
			this.drawComHand(rsp);
		}, 100);
	}
	blinkOff() {
		const { resItvID, ctx, image, wmgn } = this;
		clearInterval(resItvID);
		ctx.drawImage(image, 343, 261, 68, 52, wmgn + 343, 261, 68, 52);
		ctx.drawImage(image, 646, 260, 68, 52, wmgn + 646, 260, 68, 52);
		ctx.drawImage(image, 343, 503, 68, 52, wmgn + 343, 503, 68, 52);
		ctx.drawImage(image, 646, 503, 68, 52, wmgn + 646, 503, 68, 52);
	}
	blinkResult(result) {
		const handSels = { win:{'R':1,'S':2,'P':0}, lose:{'R':2,'S':0,'P':1}, draw:{'R':0,'S':1,'P':2} };
		this.drawComHand(handSels[result][this.myHand]);
		this.blinkOff();
		const drawBlink = this.getDrawBlink(result);
		drawBlink();
		this.resItvID = setInterval(drawBlink, 500);
	}
	getDrawBlink(result) {
		const { wmgn, ctx, image } = this;
		const self = this;
		const p = [{
				winl:[1024, 510, 68, 52, wmgn + 343, 261, 68, 52],
				winr:[1092, 510, 68, 52, wmgn + 646, 260, 68, 52],
				lose:[1024, 562, 68, 52, wmgn + 343, 503, 68, 52],
				draw:[1092, 562, 68, 52, wmgn + 646, 503, 68, 52],
			},
			{
				winl:[343, 261, 68, 52, wmgn + 343, 261, 68, 52],
				winr:[646, 260, 68, 52, wmgn + 646, 260, 68, 52],
				lose:[343, 503, 68, 52, wmgn + 343, 503, 68, 52],
				draw:[646, 503, 68, 52, wmgn + 646, 503, 68, 52],
			}];
		let i = 0;
		return function() {
			i++;
			let d = p[i % 2][result === 'win' ? 'winl' : result];
			ctx.drawImage(image, d[0], d[1], d[2], d[3], d[4], d[5], d[6], d[7], d[8]);
			if (result === 'win') {
				d = p[i % 2]['winr'];
				ctx.drawImage(image, d[0], d[1], d[2], d[3], d[4], d[5], d[6], d[7], d[8]);
			}
			if (i === 7) clearInterval(self.resItvID);
		};
	}
}

const view = new JFView('canvas', 'base');
```

## 解説

### 概要

- canvasに、あらかじめ用意した画像をペタペタする方針
- ルーレット部分だけはText描画
- ライブラリの類は使いませんでした

### コンストラクタ

- View制御クラスの中でゲームロジックの制御クラスをインスタンス化
    - ロジックのコールバックを登録
        - onstart: グーチョキパーをてけてけする
        - onwin: WINをピカピカする
        - onlose: LOSEをピカピカする、ピカピカが終わったらデモに戻る
        - ondraw: DRAWをピカピカする
        - onspin: ルーレット表示
        - onstop: デモに戻る
    - canvas初期化: 描画領域とウィンドウ領域のアスペクト比あわせ、初期描画
    - テキスト描画のための各種初期化
    - クリックイベントの登録

### メソッド

- drawInit: 初期描画、ゲームの有効領域は1:1のため横長の場合はパターンで埋める
- drawDemo: メダルを入れるまでの、ゲームデモ画面。グーチョキパーをゆっくり繰り返す
- drawComHand: 画面中央にグー／チョキ／パーのいずれかを表示する処理
- drawRoulette: 12個の数字を描画する
- onpress: グーチョキパーボタンを押した時の処理
- drawButton: ボタンをポチッとな、の描画処理
- drawComThinking: グーチョキパーを高速描画
- blinkOFF: WIN/LOSE/DRAWのピカピカを止める
- blinkResult: WIN/LOSE/DRAWのピカピカ描画処理
- getDrawBlink: ピカピカ描画処理の本体（functionを返却）

### ポイント

- 繰り返しになりますけど「メダル枚数表示」とか、まだ未完成部分がありますよ
- iOS Safariだとwindowのclickイベントは処理できない　→　canvasでaddEventListenerする
- 画面全体を描画領域にしてみた。余白を埋めたり座標変換したり、まあまあ頭使いました
- ルーレットの数字描画、最終的には数字ごとに角度を決めることで調整（degs定義を参照）
- windowのリサイズには対応していない（そこまで需要ないでしょ）
- 可読性を損なわない程度に短くしたつもり（描画のあたりはもうちょっとどうにかできそう）
- 貴重な人生の時間をジャンケンゲーム制作に費やすアラフィフ

# すべてのソース

以下のソースと画像ファイルがあればゲーム試せます。

```html
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>ジャンケンマンフィーバー2024</title>
    <style>
body {
	margin: 0;
	overflow: hidden;
}
#canvas {
	margin: auto;
	width: 100vh;
	height: 100vh;
}
#base {
	display: none;
}
    </style>
</head>
<body>
	<canvas id="canvas"></canvas>
	<img id="base" src="rps.png" />
	<script>
class JankenFever {
	#status;
	constructor(ms = 100) {
		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;
		this.onspin = null;
		this.onstop = null;
		this.interval = ms;
		this.ridx = 0;
	}
	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);
			this.status = result.next;
			result.callback ? result.callback() : null;
			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) {
			this.ridx = this.rnd(this.ROULETTE.length);
			this.scnt = 0;
			this.riid = setInterval(()=>{
				this.onspin ? this.onspin(this.ridx, this.ROULETTE) : null;
				this.ridx = (this.ridx + 1) % this.ROULETTE.length;
				if (this.scnt++ > 31) {
					clearInterval(this.riid);
					this.stop();
				}
			}, this.interval);
			return { result: true, medal: this.medal, pmedal: this.player.medal };
		} else { return { result: false, status: this.status }; }
	}
	stop() {
		if (this.status == this.STS.SPINING) {
			const medal = this.ROULETTE[this.ridx];
			this.medal -= medal;
			this.player.medal += medal;
			this.status = this.STS.STANDBY;
			this.onstop ? this.onstop() : null;
			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); }
}

class JFView {
	constructor(cid, iid) {
		this.game = new JankenFever(100);
		this.game.onstart = ()=>this.drawComThinking();
		this.game.onwin = ()=>{
			clearInterval(this.plyItvID);
			this.canInsertMedal = true;
			this.blinkResult('win');
		};
		this.game.onlose = ()=>{
			clearInterval(this.plyItvID);
			this.canInsertMedal = true;
			this.blinkResult('lose');
			this.plyItvID = setTimeout(()=>this.drawDemo(), 4000);
		};
		this.game.ondraw = ()=>{
			clearInterval(this.plyItvID);
			this.blinkResult('draw');
			this.plyItvID = setTimeout(()=>this.drawComThinking(false), 1500);
		};
		this.game.onspin = (ri, rm) => this.drawRoulette(ri, rm);
		this.game.onstop = () => this.plyItvID = setTimeout(()=>this.drawDemo(), 3000);
		this.canvas = document.getElementById(cid);
		this.ctx = this.canvas.getContext('2d');
		this.image = document.getElementById(iid);
		this.canvas.width = 1024 * (window.innerWidth / window.innerHeight);
		this.canvas.height = 1024;
		this.canvas.style.width = window.innerWidth + 'px';
		this.canvas.style.height = window.innerHeight + 'px';
		this.wmgn = Math.floor((this.canvas.width - 1024) / 2);
		this.image.addEventListener('load', e=>this.drawInit());
		this.canInsertMedal = true;
		this.plyItvID;
		this.resItvID;
		this.myHand = '';
		this.cx = (canvas.width - 1024) / 2 + 536;
		this.cy = 411;
		this.r = 130;
		this.degs = [-76,-47,-15,17,49,77,102,131,163,-165,-133,-101];
		this.fs = 28;
		this.ctx.font = `bold ${this.fs}px Impact`;
		this.ctx.lineWidth = 2.5;
		this.canvas.addEventListener('click', (e) => {
			const cvsx = Math.floor(1024 * (e.x / window.innerHeight) - this.wmgn);
			const cvsy = Math.floor(1024 * (e.y / window.innerHeight));
			if (cvsx>440 && cvsx<615 && cvsy>705 && cvsy<925 && this.canInsertMedal) {
				clearInterval(this.plyItvID);
				this.game.start();
			}
			if (cvsx>355 && cvsx<441 && cvsy>586 && cvsy<632) this.onpress('R');
			if (cvsx>487 && cvsx<573 && cvsy>586 && cvsy<632) this.onpress('S');
			if (cvsx>621 && cvsx<707 && cvsy>586 && cvsy<632) this.onpress('P');
		});
	}
	drawInit() {
		const { ctx, image, wmgn } = this;
		ctx.drawImage(image, 0, 0, 1, 1024, 0, 0, wmgn + 1, 1024);
		ctx.drawImage(image, 1022, 0, 1, 1024, wmgn + 1023, 0, wmgn + 1, 1024);
		ctx.drawImage(image, 0, 0, 1024, 1024, wmgn, 0, 1024, 1024);
		this.drawDemo();
	}
	drawDemo() {
		this.drawComHand(0);
		this.plyItvID = setInterval((()=>{
			let rsp = 0;
			return () => this.drawComHand(rsp = (rsp + 1) % 3);
		})(), 1000);	
	}
	drawComHand(rsp) {
		const { ctx, image, wmgn, game } = this;
		const rsp_xy = [ {x:1024,y:0}, {x:1024,y:170}, {x:1024,y:340} ][rsp];
		ctx.drawImage(image, rsp_xy.x, rsp_xy.y, 150, 170, wmgn + 448, 317, 150, 170);
		this.drawRoulette(-1, game.ROULETTE);
	}
	drawRoulette(rt = -1, rtmst) {
		const { degs, ctx, cx, cy, r, fs } = this;
		let i = 0;
		rtmst.forEach((num)=>{
			const [numStr, rad] = [num + '', Math.PI / 180 * degs[i]];
			ctx.strokeStyle = rt===i ? '#FF0' : ['#C22','#C22'][i%2];
			ctx.fillStyle = rt===i ? '#F00' : ['#FFF','#FFC'][i%2];
			const dx = Math.floor(cx + r * Math.cos(rad) - (fs * numStr.length) / 2);
			const dy = Math.floor(cy + r * Math.sin(rad) * 185/210);
			ctx.strokeText(numStr, dx, dy);
			ctx.fillText(numStr, dx, dy);
			i++;
		});
	}
	onpress(rsp) {
		this.drawButton(rsp);
		if (!this.canInsertMedal) this.game.play(this.myHand = rsp);
	}
	drawButton(rsp) {
		const { ctx, image, wmgn } = this;
		const xy = {
			R: { px:1024, py:614, bx:355, by:586 },
			S: { px:1024, py:660, bx:487, by:586 },
			P: { px:1024, py:706, bx:621, by:586 }
		}[rsp];
		ctx.drawImage(image, xy.px, xy.py, 86, 46, wmgn + xy.bx, xy.by, 86, 46);
		setTimeout( () => ctx.drawImage(image, xy.bx, xy.by, 86, 46, wmgn + xy.bx, xy.by, 86, 46), 150);
	}
	drawComThinking(flush = true) {
		this.canInsertMedal = false;
		flush ? this.blinkOff() : null;
		let rsp = 0;
		this.plyItvID = setInterval(() => {
			rsp = ([0,1,2].filter(i=>i!==rsp))[Math.round(Math.random())];
			this.drawComHand(rsp);
		}, 100);
	}
	blinkOff() {
		const { resItvID, ctx, image, wmgn } = this;
		clearInterval(resItvID);
		ctx.drawImage(image, 343, 261, 68, 52, wmgn + 343, 261, 68, 52);
		ctx.drawImage(image, 646, 260, 68, 52, wmgn + 646, 260, 68, 52);
		ctx.drawImage(image, 343, 503, 68, 52, wmgn + 343, 503, 68, 52);
		ctx.drawImage(image, 646, 503, 68, 52, wmgn + 646, 503, 68, 52);
	}
	blinkResult(result) {
		const handSels = { win:{'R':1,'S':2,'P':0}, lose:{'R':2,'S':0,'P':1}, draw:{'R':0,'S':1,'P':2} };
		this.drawComHand(handSels[result][this.myHand]);
		this.blinkOff();
		const drawBlink = this.getDrawBlink(result);
		drawBlink();
		this.resItvID = setInterval(drawBlink, 500);
	}
	getDrawBlink(result) {
		const { wmgn, ctx, image } = this;
		const self = this;
		const p = [{
				winl:[1024, 510, 68, 52, wmgn + 343, 261, 68, 52],
				winr:[1092, 510, 68, 52, wmgn + 646, 260, 68, 52],
				lose:[1024, 562, 68, 52, wmgn + 343, 503, 68, 52],
				draw:[1092, 562, 68, 52, wmgn + 646, 503, 68, 52],
			},
			{
				winl:[343, 261, 68, 52, wmgn + 343, 261, 68, 52],
				winr:[646, 260, 68, 52, wmgn + 646, 260, 68, 52],
				lose:[343, 503, 68, 52, wmgn + 343, 503, 68, 52],
				draw:[646, 503, 68, 52, wmgn + 646, 503, 68, 52],
			}];
		let i = 0;
		return function() {
			i++;
			let d = p[i % 2][result === 'win' ? 'winl' : result];
			ctx.drawImage(image, d[0], d[1], d[2], d[3], d[4], d[5], d[6], d[7], d[8]);
			if (result === 'win') {
				d = p[i % 2]['winr'];
				ctx.drawImage(image, d[0], d[1], d[2], d[3], d[4], d[5], d[6], d[7], d[8]);
			}
			if (i === 7) clearInterval(self.resItvID);
		};
	}
}

const view = new JFView('canvas', 'base');

</script>
</body>

</html>
```

動かすとこんな感じ。

![RPReplay_Final1714893101.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/127776/bf7dc3d7-0d76-f117-da62-b0609682629d.gif)


# 次回予告

- メダル表示はどうしようね、考え中
- 音声はVoiceVOXでずんだもんにお願い中
- できあがったら自前のサイトにアップします（もちろんアフィリエイトを添えて笑）

追記:
https://qiita.com/tri-comma/items/53ba6d47e34e1d73d86d
　「 javascriptで作るゲーム【ジャンケンマンフィーバー】完結編（音声出力） 」
続きの記事を書きました。