※記事のタイトルをクリックして、個別でページを開かないと、正しく動作しません。
前バージョン
変更点
- タイミングライトのインデックス管理をGameControllerでさせていましたが、TimingBarクラスに次のライトを点灯させるメソッドを追加したので、GameControllerは初期位置だけ決めればよくなりました。
- また、phaseCounterが11になるタイミングで次のフェーズに移行していましたが、0になるタイミングで次のフェーズに移行するように変更しました。
- タイミングスピードごとに、初期値をいろいろといじって、成立させていましたが、上記二つの変更により、だいぶすっきりさせることが出来ました。
- startShootメソッドが大きくなったので、何個かのメソッドに分割しました。
感想
- 最初、AIに相談したら、Strategyパターンを使いましょうと言われました。ですが、もっと根本的になにかが間違っていると思ったので、もっと単純化して考え直しました。
- 1フェーズ12カウントの設定でしたが、わかりやすくするために1フェーズ4カウントにして考えてみたところ、1カウント目でフェーズを進めた方が、タイミングスピード普通と遅いの時に都合が良いことが分かりました。
クラス図
変更点なし

index.html
<!doctype html> <html lang="ja"> <head> <meta charset="UTF-8" /> <link rel="icon" type="image/svg+xml" href="/janken.svg" /> <link rel="stylesheet" href="./src/style.css" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>janken</title> </head> <body> <div id="app"></div> <script type="module" src="/src/JankenGame.ts"></script> </body> </html>
JankenGame.ts
import { GameController } from "./GameController"; import { GameScreen } from "./GameScreen"; /** * ジャンケンゲームのメインクラス */ class JankenGame { /** * コンストラクタ * @param parent ジャンケンゲームの親要素 */ constructor(parent: HTMLElement){ // ゲーム画面 new GameScreen(parent); // ゲームコントローラー new GameController(); } } // app要素を取得 const appElement = document.querySelector('#app') as HTMLElement; // ジャンケンゲーム作成 new JankenGame(appElement);
EventBus.ts
import type { EventMap } from "./types"; type Callback<T> = (payload: T) => void; /** * Pub/Subパターンの中核となるイベントバス */ export class EventBus { /** イベント購読者マップ */ private listeners: { [K in keyof EventMap]?: Callback<EventMap[K]>[] } = {}; /** * イベントを購読(Subscribe)する * @param eventName イベント名 * @param callback イベント発生時に実行されるコールバック関数 */ subscribe<K extends keyof EventMap>( eventName: K, callback: Callback<EventMap[K]> ): void { if (!this.listeners[eventName]) { this.listeners[eventName] = []; } this.listeners[eventName]!.push(callback); } /** * イベントを発行(Publish)する * @param eventName イベント名 * @param payload イベントで渡すデータ */ publish<K extends keyof EventMap>( eventName: K, payload: EventMap[K] ): void { if (!this.listeners[eventName]) { console.log(`[EventBus] No subscribers for event: "${eventName}"`); return; } this.listeners[eventName]!.forEach((callback) => { try { callback(payload); } catch (error) { console.error(`[EventBus] Error in subscriber for event "${eventName}":`, error); } }); } } // アプリケーション全体で一つのインスタンスを共有する export const appEventBus = new EventBus();
GameController.ts
import { appEventBus } from "./EventBus"; import { JankenRule } from "./JankenRule"; import { TimingBar } from "./TimingBar"; import { BattlePhase, GameResult, HandKind, TimingSpeed, type PutTiming, type SetResultEvent } from "./types"; import { randRange } from "./utils"; /** * ゲームコントローラー */ export class GameController{ /** イベントバス */ private eventBus = appEventBus; /** 自分の手 */ private myHand:HandKind = HandKind.None; /** 相手の手 */ private computeHand:HandKind = HandKind.None; /** タイマーID */ private intervalId:number = 0; /** フェーズ */ private phase:number = BattlePhase.INIT; /** フェーズカウンター */ private phaseCounter:number = 0; /** 点灯パターン */ private lightingPattern:TimingSpeed = TimingSpeed.First; /** 点灯更新頻度 */ private lightingRefreshRate: number = 1; /** 手を出したか */ private isPutHand:boolean = false; /** 手を出したタイミング */ private putTiming:PutTiming; /** * コンストラクタ */ constructor(){ // 手を出したタイミング初期化 this.putTiming = {lightIndex:-1, phase:BattlePhase.INIT}; // 点灯パターン変更イベントハンドラー設定 this.eventBus.subscribe('change-lighting-pattern', (pattern) => { this.lightingPattern = pattern; }); this.eventBus.subscribe('set-put-light-index', (index) => { this.putTiming.lightIndex = index; }); // ゲームスタートイベントハンドラー設定 this.eventBus.subscribe('battle-started', () => { this.startShoot(randRange(30, 50)) }); // ハンドボタンクリックイベントハンドラー設定 this.eventBus.subscribe('hand-Button-Clicked', (handKind) => { // 出した手を設定 this.clickHandButton(handKind); }); } /** * ジャンケン開始 * @param interval ランプ点灯間隔 */ private startShoot(interval:number){ // すでに開始していたら何もしない if(this.intervalId !== 0){ return; } // 初期化 this.initializeShoot(); // タイマー開始 this.intervalId = setInterval(() => { this.shootLoopTick(); }, interval); } /** * ジャンケン初期化 */ private initializeShoot(){ // 初期化 this.myHand = HandKind.None; this.computeHand = HandKind.None; this.isPutHand = false; this.putTiming.phase = BattlePhase.INIT; this.phase = BattlePhase.INIT; this.phaseCounter = 1; // タイミングパターンによって変わる値 this.lightingRefreshRate = 1; let initLightIndex = -1; if(this.lightingPattern === TimingSpeed.Normal){ this.lightingRefreshRate = 2; initLightIndex = 5; }else if(this.lightingPattern === TimingSpeed.Slow){ this.lightingRefreshRate = 4; initLightIndex = 2; } this.eventBus.publish('timing-light-on', initLightIndex); } /** * 1フェーズカウントごとの処理 */ private shootLoopTick(){ // ライトを点灯 if(this.phaseCounter % this.lightingRefreshRate === 0){ this.eventBus.publish('timing-light-next', undefined); } // フェーズカウンターが最初に戻ったら if(this.phaseCounter <= 0){ // フェーズを進める this.advancePhase(); } // フェーズカウンターを進める this.phaseCounter++; if(this.phaseCounter >= TimingBar.PHASE_COUNT){ this.phaseCounter = 0; } } /** * フェーズを進める */ private advancePhase() { // フェーズを進める this.phase++; // フェーズ変更イベント発行 this.eventBus.publish('phase-changed', this.phase); // フェーズに応じた処理 this.handlePhaseProcess(); } /** * 現在のフェーズに応じた処理を実行 */ private handlePhaseProcess() { switch (this.phase) { case BattlePhase.JAN: // 自分の手をクリア:「じゃん」 this.myHand = HandKind.None; this.putTiming.phase = BattlePhase.INIT; this.putTiming.lightIndex = -1; break; case BattlePhase.PON: case BattlePhase.SYO: // 相手の手を出す:「ぽん」または「しょ」 this.putComputeHand(); break; case BattlePhase.EMPTY2: case BattlePhase.EMPTY3: // 勝敗判定フェーズ this.checkGameResult(); break; } } /** * 勝敗を判定し、終了か継続(あいこ)か判定 */ private checkGameResult() { const resultEvent = this.getResult(); if (resultEvent.result !== GameResult.Draw) { // 勝敗がついた場合:終了 this.stopShoot(); this.setResult(resultEvent); } else { // あいこの場合:継続 // 判定フェーズが EMPTY3 (あいこでしょの後) なら、EMPTY2 (ポン直後と同じ状態) に戻してループさせる if (this.phase === BattlePhase.EMPTY3) { this.phase = BattlePhase.EMPTY2; } this.resetHandButton(); } } /** * ジャンケン終了 */ private stopShoot(){ clearInterval(this.intervalId); this.intervalId = 0; // バトル終了イベント発行 this.eventBus.publish('battle-ended', undefined); } /** * ハンドボタンクリックハンドラー * @param handKind 手の種類 */ private clickHandButton(handKind:HandKind){ if(this.isPutHand){ return; } // 自分の手を保持 this.myHand = handKind; // 手を出したフェーズを保持 this.putTiming.phase = this.phase; // 自分の手を表示 this.eventBus.publish('set-my-hand', handKind); // 点灯しているランプの色を変更 this.eventBus.publish('put-light-on', undefined); // 「けん」また「こで」以降に手を出したら、もう次の手は出せない if((this.phase >= BattlePhase.KEN && this.phase <= BattlePhase.PON) || (this.phase >= BattlePhase.KODE && this.phase <= BattlePhase.SYO)){ this.isPutHand = true; // ハンドボタンを非表示 this.eventBus.publish('hide-hand-buttons', undefined); } } /** * あいこの時に手を出せるようにする */ private resetHandButton(){ this.myHand = HandKind.None; this.computeHand = HandKind.None; this.isPutHand = false; this.putTiming.phase = BattlePhase.INIT; this.putTiming.lightIndex = -1; this.eventBus.publish('show-hand-buttons', undefined); } /** * 相手の手を出す */ private putComputeHand(){ // ランダムで手を取得 this.computeHand = JankenRule.getRandomHand(); // 「ぽん」のタイミングより早く、自分が手を出していたら if(this.myHand !== HandKind.None){ // 勝てる手を取得 this.computeHand = JankenRule.getWindHand(this.myHand); } // 相手の手を表示 this.eventBus.publish('set-compute-hand', this.computeHand); } private getResult(){ const retResult:SetResultEvent = {reason:"", result:GameResult.Draw}; console.log(this.putTiming) // ハンドボタンが押されていない if(this.myHand === HandKind.None){ // 負けを表示 retResult.reason = "手を出してないので"; retResult.result = GameResult.Lose; }else{ // 勝敗を取得 const result = JankenRule.getResult(this.myHand, this.computeHand); retResult.result = result; // 出すタイミングが遅かった場合 if((this.putTiming.phase == BattlePhase.PON && this.putTiming.lightIndex < 11) || (this.putTiming.phase == BattlePhase.SYO && this.putTiming.lightIndex < 11)){ retResult.reason = '後出しなので'; retResult.result = GameResult.Lose; }else if((this.putTiming.phase >= BattlePhase.JAN && this.putTiming.phase <= BattlePhase.KEN) || (this.putTiming.phase >= BattlePhase.AI && this.putTiming.phase <= BattlePhase.KODE)){ // 出すタイミングが早かった場合 retResult.reason = '出すのが速すぎて'; retResult.result = GameResult.Lose; } } return retResult; } /** * 結果表示 */ private setResult(result:SetResultEvent){ // 結果表示イベント発行 this.eventBus.publish('set-result', result); } }
GameScreen.ts
import { BattleArea } from "./BattleArea"; import { appEventBus } from "./EventBus"; import { HandButtonArea } from "./HandButtonArea"; import { Header } from "./Header"; /** * ゲーム画面 */ export class GameScreen{ /** イベントバス */ private eventBus = appEventBus; /** ヘッダー */ private header: Header; /** バトルエリア */ private battleArea: BattleArea; /** スタートボタン */ private startButton: HTMLButtonElement; /** グーチョキパーボタン */ private handButtonArea: HandButtonArea; constructor(parent: HTMLElement){ // ヘッダー this.header = new Header(); parent.appendChild(this.header.getElement()); // バトルエリア this.battleArea = new BattleArea(); parent.appendChild(this.battleArea.getElement()); // 開始ボタン this.startButton = document.createElement("button"); this.startButton.classList.add('start-button', 'hand-button') this.startButton.textContent = "勝負!"; this.startButton.addEventListener('click', ()=>{ // スタートボタン非表示 this.startButton.classList.add('hidden'); // ハンドボタン表示 this.handButtonArea.getElement().classList.remove('hidden'); // 表示テキスト初期化 this.battleArea.textClear(); // すべてのランプを消灯 this.battleArea.lightOffAll(); // 開始イベント発行 this.eventBus.publish('battle-started', undefined); }) parent.appendChild(this.startButton); // グー・チョキ・パーボタン this.handButtonArea = new HandButtonArea(); parent.appendChild(this.handButtonArea.getElement()); // 準備状態にする this.setLady(); // ジャンケン終了イベントハンドラー設定 this.eventBus.subscribe('battle-ended', () => { this.setLady(); }); } /** * バトル準備 */ private setLady(){ // ハンドボタンエリアを非表示 this.handButtonArea.getElement().classList.add('hidden'); // スタートボタンを表示 this.startButton.classList.remove('hidden'); } }
Header.ts
import { appEventBus } from "./EventBus"; import { TimingSpeed, TimingSpeedNames } from "./types"; /** * タイトル・説明を表示 */ export class Header{ /** ルート要素 */ private element:HTMLElement; /** タイトル要素 */ private title:HTMLElement; /** 説明要素 */ private explanation:HTMLElement; /** イベントバス */ private eventBus = appEventBus; private debugArea:HTMLElement; /** * コンストラクタ */ constructor(){ // 自身の要素を作成 this.element = document.createElement("div"); // タイトル this.title = document.createElement('h1'); this.title.textContent = 'ジャンケンゲーム'; this.element.appendChild(this.title); // 説明 this.explanation = document.createElement('div'); this.explanation.className ='explanation'; this.explanation.innerHTML = ` <div>勝負ボタンを押すと「さいしょはグー、じゃんけんぽん」の掛け声が始まります。</div> <div>「ぽん」のタイミングで出したい手のボタンを押してください。</div> <div>上側が相手、下側があなたが出した手になります。</div> <div>タイミングが早いと、超反応速度であなたの手を見られて、必ず負けてしまいます。</div> <div>タイミングが遅いと、後出しだといちゃもんをつけられて、負けてしまいます。</div> <div>タイミングよくボタンを押しましょう。</div>`; this.element.appendChild(this.explanation); // 点灯パターン const fieldset = document.createElement('fieldset'); this.element.appendChild(fieldset); const legend = document.createElement('legend'); legend.textContent = 'タイミングスピード'; fieldset.appendChild(legend); const div = document.createElement('div'); fieldset.appendChild(div); let radioCounter = 1; Object.values(TimingSpeed).forEach(value => { const radio1 = document.createElement('input'); radio1.type = 'radio'; radio1.name = 'pattern'; radio1.value = value; radio1.id = 'pattern-' + value; if(radioCounter === 1) radio1.checked = true; div.appendChild(radio1); const label1 = document.createElement('label'); label1.htmlFor = 'pattern-' + value; label1.textContent = TimingSpeedNames[value]; div.appendChild(label1); radio1.addEventListener('change', (e) => { const target = e.target as HTMLInputElement; this.eventBus.publish('change-lighting-pattern', target.value as TimingSpeed); }); radioCounter++; }); // デバッグエリア this.debugArea = document.createElement('div'); this.debugArea.id = 'debug-area'; this.element.appendChild(this.debugArea); this.eventBus.subscribe('debug-log', (msg) => { const p = document.createElement('p'); p.textContent = msg; this.debugArea.innerHTML = ''; this.debugArea.appendChild(p); }); } /** * 要素取得 * @returns 要素 */ getElement():HTMLElement{ return this.element; } }
BattleArea.ts
import { appEventBus } from "./EventBus"; import { TimingBar } from "./TimingBar"; import { BattlePhase, GameResult, HandButtonsDefine, HandKind, ResultNames } from "./types"; /** * バトルエリア */ export class BattleArea{ /** ルート要素 */ private element:HTMLElement; /** イベントバス */ private eventBus = appEventBus; /** タイミングバー */ private timingbar:TimingBar; /** 掛け声エリア */ private verbalCueArea:HTMLElement; /** 掛け声 */ private verbalCue:HTMLElement; /** 勝敗結果 */ private result:HTMLElement; /** 自分の手 */ private myHand:HTMLElement; /** 相手の手 */ private computeHand:HTMLElement; /** * コンストラクタ */ constructor(){ // ルート要素 this.element = document.createElement('div'); this.element.id = 'battle-area'; // タイミングバー this.timingbar = new TimingBar(); this.element.appendChild(this.timingbar.getElement()); // ハンドエリア const handArea = document.createElement('div'); handArea.id = 'hand-area'; this.element.appendChild(handArea); // 相手の手 this.computeHand = document.createElement('div'); this.computeHand.id = 'compute-hand'; handArea.appendChild(this.computeHand); // 自分の手 this.myHand = document.createElement('div'); this.myHand.id = 'my-hand'; handArea.appendChild(this.myHand); // 掛け声エリア this.verbalCueArea = document.createElement('div'); this.verbalCueArea.id = 'verbal-cue'; handArea.appendChild(this.verbalCueArea); // 掛け声 this.verbalCue = document.createElement('span'); this.verbalCueArea.appendChild(this.verbalCue); // 勝敗結果 this.result = document.createElement('span'); this.result.id = 'result'; this.verbalCueArea.appendChild(this.result); // 相手の手を表示 this.eventBus.subscribe('set-compute-hand', (handKind) => { this.setComputeHand(handKind); }); // 自分の手を表示 this.eventBus.subscribe('set-my-hand', (handKind) => { this.setMyHand(handKind); }); // 結果表示 this.eventBus.subscribe('set-result', (data) => { this.setResult(data.reason, data.result); }); // フェーズ変更イベントハンドラー設定 this.eventBus.subscribe('phase-changed', (phase) => { // フェーズに合わせて掛け声を表示 if(phase === BattlePhase.SAI){ this.setVerbalCue("さい"); }else if(phase === BattlePhase.SYOHA){ this.setVerbalCue("しょは"); }else if(phase === BattlePhase.GUU){ this.setVerbalCue("グー"); this.setComputeHand(HandKind.Rock); }else if(phase === BattlePhase.EMPTY1){ this.setVerbalCue("") this.timingbar.putLightOff(); }else if(phase === BattlePhase.JAN){ this.textClear(); this.setVerbalCue("じゃん") }else if(phase === BattlePhase.KEN){ this.setVerbalCue("けん") }else if(phase === BattlePhase.PON){ this.setVerbalCue("ぽん") }else if(phase === BattlePhase.EMPTY2){ // 何もしない }else if(phase === BattlePhase.AI){ this.textClear(); this.setVerbalCue('あい'); }else if(phase === BattlePhase.KODE){ this.setVerbalCue('こで'); }else if(phase === BattlePhase.SYO){ this.setVerbalCue('しょ'); } }); } /** * 要素取得 * @returns 要素 */ getElement(): HTMLElement{ return this.element; } /** * 自分の手を表示 * @param hand 自分の手 */ private setMyHand(hand:HandKind){ this.myHand.textContent = HandButtonsDefine[hand].name; } /** * 結果を表示 * @param reson 理由 * @param result 勝敗 */ private setResult(reson:string, result:GameResult){ this.verbalCue.textContent = reson; this.result.textContent = ResultNames[result]; this.setResultColor(result); } /** * 勝敗に合わせた文字色を設定 * @param result 勝敗 */ private setResultColor(result:GameResult){ for(const gameResult of Object.values(GameResult)){ if(result === gameResult){ this.result.classList.add(result); }else{ this.result.classList.remove(gameResult); } } } /** * タイミングバーをすべて消灯 */ lightOffAll(){ this.timingbar.lightOffAll(); } /** * 表示テキストをクリア */ textClear(){ this.myHand.textContent = ""; this.computeHand.textContent = ""; this.verbalCue.textContent = ""; this.result.textContent = ""; } /** * 掛け声表示 * @param text 掛け声 */ private setVerbalCue(text:string){ this.verbalCue.textContent = text; } /** * 相手の手を表示 * @param hand 相手の手 */ private setComputeHand(hand:HandKind){ this.computeHand.textContent = HandButtonsDefine[hand].name; } }
TimingBar.ts
import { appEventBus } from "./EventBus"; /** * タイミングバー */ export class TimingBar{ /** 左右ランプのカウント数 */ private static readonly LAMP_COUNT = 11; /** 1フェーズのカウント数 */ public static readonly PHASE_COUNT = TimingBar.LAMP_COUNT + 1; /** ランプの総数 */ private static readonly TOTAL_LAMP_COUNT = TimingBar.LAMP_COUNT * 2 + 1; /** ルート要素 */ private element:HTMLElement; /** イベントバス */ private eventBus = appEventBus; /** 中央ランプ */ private centerLamp!:HTMLElement; /** 左側ランプ */ private leftLamps:HTMLElement[] = []; /** 右側ランプ */ private rightLamps:HTMLElement[] = []; /** 点灯ランプ */ private lightOnIndex:number = -1; /** 手を出したタイミング */ private putIndex:number = -1; /** * コンストラクタ */ constructor(){ // ルート要素 this.element = document.createElement('div'); this.element.id = 'timingbar'; // 左に12個、右に12個、中央1個 for(let index = 0; index < TimingBar.TOTAL_LAMP_COUNT; index++){ const lamp = document.createElement('span'); lamp.classList.add('lamp'); if(index === TimingBar.LAMP_COUNT){ // 中央 lamp.classList.remove('lamp') lamp.classList.add('center-lamp') lamp.id = 'center-lamp'; this.centerLamp = lamp; }else if(index < TimingBar.LAMP_COUNT){ // 左側 lamp.id = 'left-lamp' + (index + 1); this.leftLamps.push(lamp); }else{ // 右側 lamp.id = 'rignt-lamp' + (TimingBar.TOTAL_LAMP_COUNT- index); this.rightLamps.unshift(lamp); } this.element.appendChild(lamp) } // タイミングライト点灯イベントハンドラー設定 this.eventBus.subscribe('timing-light-on', (index) => { this.timingLightOn(index); }); // 次のタイミングライトを点灯するイベントハンドラー設定 this.eventBus.subscribe('timing-light-next', () => { let nextIndex = this.lightOnIndex + 1; if(nextIndex >= this.getLength()){ nextIndex = 0; } this.timingLightOn(nextIndex); }); // 手を出したときのライト色変更イベントハンドラー設定 this.eventBus.subscribe('put-light-on', () => { this.putLightOn(); this.eventBus.publish('set-put-light-index', this.lightOnIndex); }); } /** * 要素取得 * @returns 要素 */ getElement():HTMLElement{ return this.element; } /** * ライト点灯 * @param lamps ライト * @param tokens クラス名 */ private lightOn(lamps:HTMLElement[], ...tokens: string[]){ for(const lamp of lamps){ lamp.classList.add(...tokens); } } /** * ライト消灯 * @param lamps ライト * @param tokens クラス名 */ private lightOff(lamps:HTMLElement[], ...tokens: string[]){ for(const lamp of lamps){ lamp.classList.remove(...tokens); } } /** * インデックスを指定してライトを消灯 * @param index インデックス */ private lightOffByIndex(index:number){ // ライト取得 const lamps = this.getLamps(index); // 消灯 this.lightOff(lamps, 'light-on', 'put-light'); } /** * すべてのライトを消灯 */ lightOffAll(){ for(let index = 0; index < this.getLength(); index++){ this.lightOffByIndex(index); } // インデックスを初期化 this.lightOnIndex = -1; this.putIndex = -1; } /** * ライトの数 * @returns ライトの数 */ private getLength():number{ return this.leftLamps.length + 1; } /** * インデックスを指定してタイミングライト消灯 * @param index インデックス */ private timingLightOffByIndex(index:number){ // ライト取得 const lamps = this.getLamps(index); // 消灯 this.lightOff(lamps, 'light-on'); } /** * インデックスを指定してタイミングライト点灯 * @param index インデックス */ private timingLightOn(index:number){ // 前回のライトを消灯 this.timingLightOffByIndex(this.lightOnIndex); // ライト取得 const lamps = this.getLamps(index); // 点灯 this.lightOn(lamps, 'light-on'); // インデックスを保持 this.lightOnIndex = index; } /** * インデックスを指定して、手を出した時の色のライトを消灯 * @param index インデックス */ private putLightOffByIndex(index:number){ // ライト取得 const lamps = this.getLamps(index); // 消灯 this.lightOff(lamps, 'put-light'); } /** * 現在点灯しているライトの色を、手を出したときの色にする */ private putLightOn(){ // 現在点灯しているランプの色を変える this.putLightOnByIndex(this.lightOnIndex); } putLightOff(){ // 手を出したときのライトを消灯 this.putLightOffByIndex(this.putIndex); } /** * インデックスを指定して、手を出したときの色にする * @param index インデックス */ private putLightOnByIndex(index:number){ // 前回のライト消灯 this.putLightOffByIndex(this.putIndex); // ライト取得 const lamps = this.getLamps(index); // 点灯 this.lightOn(lamps, 'put-light'); // インデックスを保持 this.putIndex = index; } /** * インデックスで指定したランプを取得 * @param index インデックス * @returns ランプ配列 */ private getLamps(index:number): HTMLElement[]{ if(index < 0 || index >= this.getLength()){ return []; } const lamps:HTMLElement[] = []; if(index === this.getLength() - 1){ lamps.push(this.centerLamp); }else{ lamps.push(this.leftLamps[index]); lamps.push(this.rightLamps[index]); } return lamps; } }
HandButtonArea.ts
import { appEventBus } from "./EventBus"; import { HandButton } from "./HandButton"; import { HandButtonsDefine, HandKind } from "./types"; /** * グー・チョキ・パーボタン表示エリア */ export class HandButtonArea{ /** ルート要素 */ private element:HTMLElement; /** イベントバス */ private eventBus = appEventBus; /** グー・チョキ・パーボタン */ private handButtons:HandButton[] = []; /** * コンストラクタ */ constructor(){ // ルート要素 this.element = document.createElement('div'); this.element.className = 'hand-buttons'; // グーチョキパーボタンを作成 Object.values(HandButtonsDefine).forEach(value => { if(value.id !== HandKind.None){ // ボタン作成 const handButton = new HandButton(value); // 格納場所に追加 this.element.appendChild(handButton.getElement()); // ボタンを保持 this.handButtons.push(handButton); } }); // ハンドボタン表示イベントハンドラー設定 this.eventBus.subscribe('show-hand-buttons', () => { this.element.classList.remove('hidden'); }); // ハンドボタン非表示イベントハンドラー設定 this.eventBus.subscribe('hide-hand-buttons', () => { this.element.classList.add('hidden'); }); } /** * 要素取得 * @returns 要素 */ getElement():HTMLElement{ return this.element; } }
HandButton.ts
import { appEventBus } from "./EventBus"; import type { HandButtonOption, HandKind } from "./types"; /** * グー・チョキ・パーボタン */ export class HandButton{ /** 種別 */ private kind: HandKind; /** ルート要素 */ private element: HTMLElement; /** イベントバス */ private eventBus = appEventBus; /** * コンストラクタ * @param buttonDefine ボタン定義 */ constructor(buttonDefine: HandButtonOption){ // ボタン要素を作成 this.element = document.createElement('button'); // idを設定 this.element.id = buttonDefine.id; // classを設定 this.element.className = "hand-button"; // 表示名を設定 this.element.textContent = buttonDefine.name; // 種別を設定 this.kind = buttonDefine.id; // クリックイベント設定 this.element.addEventListener('click', () => { this.eventBus.publish('hand-Button-Clicked', this.kind); }); } /** * 要素取得 * @returns 要素 */ getElement(): HTMLElement { return this.element; } }
JankenRule.ts
import { GameResult, HandButtonsDefine, HandKind } from "./types"; import { randRange } from "./utils"; /** * ジャンケンのルール */ export class JankenRule{ /** * 手をランダムに取得 * @returns グー or チョキ or パー */ static getRandomHand(): HandKind { const hands = Object.values(HandKind); const randomIndex = randRange(Object.keys(HandButtonsDefine).indexOf(HandKind.Rock), Object.keys(HandButtonsDefine).indexOf(HandKind.Paper)); return hands[randomIndex]; } /** * 勝てる手を取得 * @param hand HandKind * @returns HandKind */ static getWindHand(hand: HandKind): HandKind{ if(hand == HandKind.Rock){ return HandKind.Paper; }else if(hand == HandKind.Scissors){ return HandKind.Rock; }else{ return HandKind.Scissors; } } /** * 自分と相手の手から勝敗を判定 * @param playerHand 自分の手 * @param computerHand 相手の手 * @returns 勝敗 */ static getResult(playerHand: HandKind, computerHand: HandKind): GameResult { if (playerHand === computerHand) { return GameResult.Draw; } else if ( (playerHand === HandKind.Rock && computerHand === HandKind.Scissors) || (playerHand === HandKind.Scissors && computerHand === HandKind.Paper) || (playerHand === HandKind.Paper && computerHand === HandKind.Rock) ) { return GameResult.Win; } else { return GameResult.Lose; } } }
types.ts
/** グーチョキパーボタンのオプション */ export interface HandButtonOption { id: HandKind; name: string; } /** グーチョキーパー種別 */ export const HandKind = { None: "none", Rock: "rock", Scissors: "scissors", Paper: "paper" } as const export type HandKind = typeof HandKind[keyof typeof HandKind]; /** グーチョキパーボタン定義 */ export const HandButtonsDefine: Record<HandKind, HandButtonOption> = { [HandKind.None]: {id:HandKind.None, name:""}, [HandKind.Rock]: {id:HandKind.Rock, name:"グー"}, [HandKind.Scissors]: {id:HandKind.Scissors, name:"チョキ"}, [HandKind.Paper]: {id:HandKind.Paper, name:"パー"}, } as const /** 勝敗種別 */ export const GameResult = { Win: "win", Lose: "lose", Draw: "draw" } as const export type GameResult = typeof GameResult[keyof typeof GameResult]; /** 勝敗の表示名 */ export const ResultNames: Record<GameResult, string> = { [GameResult.Win]: "勝ち", [GameResult.Lose]: "負け", [GameResult.Draw]: "あいこ", } as const /** タイミングスピード */ export const TimingSpeed = { First: 'first', Normal: 'normal', Slow: 'slow' } as const; export type TimingSpeed = typeof TimingSpeed[keyof typeof TimingSpeed]; /** タイミングスピード名 */ export const TimingSpeedNames: Record<TimingSpeed, string> = { [TimingSpeed.First]: '速い', [TimingSpeed.Normal]: '普通', [TimingSpeed.Slow]: '遅い', } as const; /** フェーズ定義 */ export const BattlePhase ={ INIT: -1, SAI: 0, SYOHA: 1, GUU: 2, EMPTY1: 3, JAN: 4, KEN: 5, PON: 6, EMPTY2: 7, AI: 8, KODE: 9, SYO: 10, EMPTY3: 11, } as const export type BattlePhase = typeof BattlePhase[keyof typeof BattlePhase]; /** 手を出したタイミング */ export interface PutTiming{ /** フェーズ */ phase:number; /** 点灯ライト */ lightIndex:number; } /** 結果表示イベントの引数 */ export interface SetResultEvent{ reason:string; result:GameResult; } /** イベント一覧 */ export interface EventMap { 'set-my-hand': HandKind; 'set-compute-hand': HandKind; 'set-result': SetResultEvent; 'battle-started': undefined; 'battle-ended': undefined; 'phase-changed': number; 'hand-Button-Clicked': HandKind; 'show-hand-buttons': undefined; 'hide-hand-buttons': undefined; 'change-lighting-pattern': TimingSpeed; 'timing-light-on': number; 'timing-light-next': undefined; 'put-light-on': undefined; 'set-put-light-index': number; 'debug-log': string; }
utils.ts
/** * 乱数取得 * @param min 最低値 * @param max 最大値 * @returns min以上max以下の乱数 */ export const randRange = (min: number, max: number): number => { return (Math.floor(Math.random() * (max - min + 1)) + min); }



