花札ゲーム「そふ花」
- 担当
- ゲームロジック・UI 実装
- URL
- https://packed7ice.github.io/sofuhana/
- ソースコード
- https://github.com/packed7Ice/sofuhana
サークルの成果物として制作した、ブラウザで遊べる花札「こいこい」ゲーム。フレームワークを使わず、素の HTML5 / CSS3 / JavaScript (ES Modules) のみで SPA として実装しました。山札・手札・場札の状態管理、役判定・得点計算、CPU の簡易 AI、チュートリアル機能までを自前で実装しています。MIT ライセンスでソースコードを公開中です。
技術スタック
HTML5 / CSS3 / JavaScript (ES Modules) / node --test / GitHub Pages
制作のポイント
- 花札「こいこい」のルール・役判定・得点計算をフルスクラッチで実装
- DOM 操作 (ui.js / dom-elements.js) とゲームロジック (state.js / card-data.js) をモジュール単位で分離
- 点数・山札残り枚数・こいこい状態から「上がり / こいこい」を判断する簡易 CPU AI
- fitApp() + visualViewport 等で画面サイズ・向きに追従するレスポンシブな盤面レイアウト
- Node.js 組み込みテストランナー (node --test) による役判定ロジックのユニットテスト
- 初心者向けのチュートリアル機能・役アシスト(ヒント表示)機能
コード抜粋
export function checkYaku(cards){
const yakuList = [];
const capturedSet = new Set(cards);
const containsAll = (names) => names.every(name => capturedSet.has(name));
const inoshikacho = ['萩に猪','紅葉に鹿','牡丹に蝶'];
if (containsAll(inoshikacho)) yakuList.push('猪鹿蝶');
if (containsAll(RED_TAN_CARDS)) yakuList.push('赤短');
if (containsAll(BLUE_TAN_CARDS)) yakuList.push('青短');
if (capturedSet.has('芒に月') && capturedSet.has('菊に盃')) yakuList.push('月見酒');
if (capturedSet.has('桜に幕') && capturedSet.has('菊に盃')) yakuList.push('花見酒');
const lights = cards.filter(c => getCardType(c)==='光' || c==='柳に小野道風');
const tane = cards.filter(c => getCardType(c)==='タネ');
const tanzaku = cards.filter(c => getCardType(c)==='短冊');
let kasu = cards.filter(c => getCardType(c)==='カス').length;
if (cards.includes('菊に盃')) kasu += 1; // 「菊に盃」はカスにも数える
const hasRain = cards.includes('柳に小野道風');
if (lights.length >= 5) yakuList.push('五光');
else if (lights.length === 4 && !hasRain) yakuList.push('四光');
else if (lights.length === 4 && hasRain) yakuList.push('雨四光');
else if (lights.length >= 3 && !hasRain) yakuList.push('三光');
if (tane.length >= 5) yakuList.push('タネ');
if (tanzaku.length >= 5) yakuList.push('短冊');
if (kasu >= 10) yakuList.push('カス');
return yakuList;
}取得札の集合から猪鹿蝶・赤短・五光などの役を判定する。光札・タネ・短冊・カスの枚数と特殊札(柳に小野道風・菊に盃)の扱いを自前で管理。
const cpuEvaluation = scoreFromCaptured(state.cpuCaptured);
if (cpuEvaluation.basePoints > 0){
let cpuEnds = false;
if (cpuEvaluation.basePoints >= 7) cpuEnds = true;
else if (state.deck.length <= 8 && cpuEvaluation.basePoints >= 5) cpuEnds = true;
else if (state.cpuHand.length === 0 || state.playerKoikoi) cpuEnds = true; // 強制上がり
else cpuEnds = Math.random() < 0.5;
if (cpuEnds){
endRound('cpu');
return;
}
state.cpuKoikoi = true;
state.cpuKoikoiBasePoints = cpuEvaluation.basePoints || 0;
showKoikoiDeclaration('cpu');
showBottomMessage('相手は「こいこい」!');
}現在の得点・山札の残り枚数・強制上がり条件を加味して、CPU が「上がり」か「こいこい」かを選択する簡易 AI。