はじめに
これは何?
Vite + TypeScript で、ドンジャラ風(同じ絵柄を3枚集める“トリプル”を3セット作ったら勝ち)のブラウザゲームを作りました。
UIは「夜桜」テーマで、BGM(MP3)の再生/ミュート切替も付けています。
この記事では ゲーム実装の要点 と GitHub Pagesでの公開でハマりやすい点(Viteのbase) を中心にまとめます。
ざっくり機能
- 人間 vs CPU
- 牌の山からツモって捨てる
- 上がり条件: 「トリプル×3(合計9枚)」を満たしたら勝ち
- BGM: 開始ボタン押下で再生(自動再生制限を回避) + ON/OFF
- GitHub Actionsでビルドして GitHub Pages へデプロイ
ディレクトリ構成(重要なところだけ)
.
├─ index.html
├─ public/
│ └─ audio/
│ └─ bgm.mp3
├─ src/
│ ├─ game.ts # 進行(start / draw / discard / CPUターン)
│ ├─ player.ts # 手牌管理 + 上がり判定
│ ├─ ui.ts # DOM描画 + BGM制御
│ └─ ...
├─ vite.config.ts # base調整(Pages向け)
└─ .github/workflows/deploy-pages.yml
ゲームロジックの要点
1) 「状態」を中心にしてUIは描画するだけにする
Game は GameState を持ち、状態が変わるたびにUIへ通知します。
UIはその状態を見てDOMを再描画するだけにすると、ロジックが読みやすくなります。
2) ゲーム開始〜ツモまでの流れ(最小)
開始時に牌を作ってシャッフルし、9枚ずつ配って、プレイヤーがツモからスタートします。
// src/game.ts(抜粋)
start(): void {
const { player, cpu } = this.state;
player.reset();
cpu.reset();
const deck = shuffleTiles(createAllTiles());
for (let i = 0; i < 9; i++) {
player.addToHand(deck.pop()!);
cpu.addToHand(deck.pop()!);
}
this.updateState({
phase: 'playerTurn',
deck,
discardPile: [],
winner: null,
message: ''
});
this.drawTile(player);
}
3) 上がり判定は「10枚になったら1枚抜いて9枚で成立するか」を全探索する
ドンジャラ系は「捨て牌の選択」があるので、手元に10枚になる瞬間があります。
そこで 1枚ずつ抜いて9枚が“トリプル×3”になるか を見ると、実装がシンプルになります。
// src/player.ts(抜粋)
isWinning(): boolean {
const allTiles = this.getAllTiles();
// 10枚の場合、1枚を除いて9枚で上がれるかチェック
if (allTiles.length === 10) {
for (let i = 0; i < allTiles.length; i++) {
const tilesWithoutOne = [...allTiles.slice(0, i), ...allTiles.slice(i + 1)];
if (this.checkWinningHand(tilesWithoutOne)) {
return true;
}
}
return false;
}
// 9枚の場合はそのままチェック
return this.checkWinningHand(allTiles);
}
private checkWinningHand(tiles: Tile[]): boolean {
const counts = new Map<TileType, number>();
for (const tile of tiles) {
counts.set(tile.type, (counts.get(tile.type) || 0) + 1);
}
let tripleCount = 0;
for (const count of counts.values()) {
if (count % 3 !== 0) return false;
tripleCount += count / 3;
}
return tripleCount === 3;
}
BGM(MP3)を入れる
1) MP3は public/ 配下に置く
Viteなら public/ 配下に置いたファイルは、そのまま静的配信されます。
- 置き場所:
public/audio/bgm.mp3
2) 自動再生制限に引っかからないよう「クリック」をトリガーに play() する
ブラウザは、ユーザー操作なしの音声再生をブロックします。
そこで「始める」ボタン押下(=ユーザー操作)で再生開始します。
また GitHub Pages のサブパス配信(https://<user>.github.io/<repo>/)でも壊れないよう、import.meta.env.BASE_URL 経由で参照します。
// src/ui.ts(抜粋)
private initBgm(): void {
const src = `${import.meta.env.BASE_URL}audio/bgm.mp3`;
const audio = new Audio(src);
audio.loop = true;
audio.preload = 'auto';
audio.volume = 0.35;
this.bgm = audio;
}
private setupEventListeners(): void {
this.elements.startBtn.addEventListener('click', () => {
this.tryStartBgm(); // ユーザー操作のタイミングで再生
this.game.start();
});
}
private tryStartBgm(): void {
if (!this.bgm) return;
if (!this.bgm.paused) return;
void this.bgm.play().catch(() => {});
}
GitHub Pages(GitHub Actions)で公開する
1) Viteの base を調整する(ここがハマりポイント)
GitHub Pages のURLが https://<user>.github.io/<repo>/ の場合、アセット参照パスがズレて白画面になりがちです。
手軽な対策は、base: './' にして相対パスで出力することです。
// vite.config.ts
export default defineConfig({
base: './',
build: { outDir: 'dist' }
})
2) Actionsでビルド→Pagesへデプロイする
dist/ を artifact としてアップロードして、actions/deploy-pages でデプロイします。
# .github/workflows/deploy-pages.yml(抜粋)
permissions:
contents: read
pages: write
id-token: write
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
cache: npm
- run: npm ci
- run: npm run build
- uses: actions/upload-pages-artifact@v3
with:
path: dist
deploy:
needs: build
runs-on: ubuntu-latest
steps:
- uses: actions/deploy-pages@v4
GitHub側で Settings → Pages → Build and deployment → GitHub Actions を選ぶと動きます。
まとめ
- 状態管理(GameState) を中心にして、UIは描画だけにすると実装が整理しやすい
- BGMは ユーザー操作をトリガーに
play()(自動再生制限対策) - GitHub Pagesでは Viteの
baseが地雷になりやすいので、base: './'がラク
次にやりたい改善案
- SE(捨てる音/ツモ音)や音量スライダー
- スマホでの操作性(誤タップ防止、UI最適化)
- CPUロジック強化(期待値で捨て牌を選ぶ、役や点数の導入)
質問・フィードバック
質問や感想はコメント欄でお気軽にどうぞ。より詳しい相談は プロフィール からどうぞ。
最後まで読んでいただき、ありがとうございました。ぜひ、このゲームを遊んでみてください!
参考リンク
技術スタック: Vite, TypeScript, HTML/CSS, GitHub Actions, GitHub Pages
