Closed6

100日チャレンジ day11 (イベントソーシングをつかったパズルゲーム)

riddle_tecriddle_tec

昨日
https://zenn.dev/gin_nazo/scraps/244831e544a295


https://blog.framinal.life/entry/2025/04/14/154104

100日チャレンジに感化されたので、アレンジして自分でもやってみます。

やりたいこと

  • 世の中のさまざまなドメインの簡易実装をつくり、バックエンドの実装に慣れる(dbスキーマ設計や、関数の分割、使いやすいインターフェイスの切り方に慣れる
  • 設計力(これはシステムのオーバービューを先に自分で作ってaiに依頼できるようにする
  • 生成aiをつかったバイブコーティングになれる
  • 実際にやったことはzennのスクラップにまとめ、成果はzennのブログにまとめる(アプリ自体の公開は必須ではないかコードはgithubにおく)

できたもの

https://github.com/lirlia/100day_challenge_backend/tree/main/day11_lights_out

riddle_tecriddle_tec

今日はイベントソーシングを使った、ライツアウトというゲームを作る

riddle_tecriddle_tec

Day 11: Lights Out Game with Event Sourcing

アプリケーション概要

「Lights Out」というパズルゲームを実装します。
このアプリケーションでは、ゲームの進行(プレイヤーの操作)をイベントとして記録するイベントソーシングパターンを採用します。
記録されたイベントを利用して、ゲームクリア後に操作手順を再現するリプレイ機能を実装します。

ゲームルール

  1. 盤面: 5x5 のグリッド状のライト(ボタン)で構成されます。
  2. 初期状態: ゲーム開始時に、いくつかのライトがランダム(または固定パターン)で点灯状態になります。
  3. 操作: プレイヤーがいずれかのライトをクリックすると、クリックしたライト自身と、その上下左右に隣接するライトの状態(オン/オフ)が反転します。盤面の端のライトをクリックした場合、存在しない隣接マスは無視されます。
  4. クリア条件: 全てのライトを消灯(オフ)状態にすること。

機能要件

  1. ゲームプレイ:
    • 5x5 の盤面を表示する。
    • ライトはオン/オフの状態に応じて見た目が変わる。
    • クリックされたライトとその隣接ライトの状態を反転させる。
    • 現在の盤面状態をリアルタイムで表示する。
    • すべてのライトが消灯したら、クリアメッセージを表示する。
    • 新しいゲームを開始するボタン(盤面リセット)を設ける。
  2. イベントソーシング:
    • ゲームの各操作(ライトのクリック)をイベント (LightToggled) として記録する。
    • イベントには、操作されたライトの位置情報(行、列)を含める。
    • ゲームの初期状態 (GameInitialized) とクリア (GameWon) もイベントとして記録する。
    • イベントはデータベース (SQLite) に永続化する。
    • 各ゲームセッションは一意のID (gameId) で識別する。
  3. リプレイ機能:
    • ゲームクリア後、または任意のタイミングで、特定の gameId のイベント履歴を取得できる。
    • 取得したイベント (LightToggled) を順に適用し、ゲームの進行を盤面上で視覚的に再生する機能を提供する。
    • リプレイは一手ずつ進める(例: 「次へ」ボタン)か、自動再生できる。

データモデル (Prisma Schema 想定)

// イベントストア用の汎用モデル
model DomainEvent {
  id        String   @id @default(cuid())
  gameId    String   // どのゲームセッションのイベントか
  type      String   // イベントの種類 (e.g., "GameInitialized", "LightToggled", "GameWon")
  payload   Json     // イベント固有のデータ (e.g., { row: 1, col: 2 } for LightToggled)
  sequence  Int      // 同一 gameId 内でのイベント発生順序
  createdAt DateTime @default(now())

  @@index([gameId, sequence]) // gameId と順序で検索するため
}

// (オプション) 現在のゲーム状態のスナップショット/リードモデル
// イベントが多い場合に、最新状態の取得を高速化するために使用
// 今回はシンプルにするため、イベント再生のみで状態を構築しても良い
// model GameState {
//   gameId      String    @id
//   boardState  Json      // 現在の盤面の状態 [[Bool]]
//   moves       Int       // 手数
//   isCompleted Boolean   @default(false)
//   updatedAt   DateTime  @updatedAt
// }

画面構成案

  1. ゲーム画面 (/):
    • 現在のゲーム盤面 (5x5 グリッド)
    • 手数カウンター
    • 「新しいゲーム」ボタン
    • クリア時に表示されるメッセージ
    • (オプション)「リプレイを見る」ボタン(クリア後 or 履歴から選択)
  2. リプレイ画面 (/replay/[gameId]):
    • 指定された gameId のゲーム盤面
    • 再生コントロール(「一手進む」「自動再生」「最初から」など)
    • 現在の再生手数/総手数

技術スタック

  • フレームワーク: Next.js (App Router)
  • 言語: TypeScript
  • データベース: SQLite
  • ORM: Prisma
  • スタイリング: Tailwind CSS
  • 状態管理: React Hooks (useState, useReducer) / イベントソーシングのリードモデル

実装方針

  1. データモデル定義: prisma/schema.prismaDomainEvent モデルを定義する。
  2. ゲームロジック実装: ライトの状態反転、クリア判定ロジックを app/_lib などに実装する。
  3. APIエンドポイント:
    • POST /api/games: 新しいゲームを開始し、初期化イベント (GameInitialized) を記録。gameId を返す。
    • POST /api/games/[gameId]/moves: プレイヤーの操作を受け取り、LightToggled イベントを記録。クリア判定も行い、必要なら GameWon イベントも記録。現在の盤面状態を返す(イベントを再生して構築)。
    • GET /api/games/[gameId]/events: 特定ゲームのイベント履歴を取得する (リプレイ用)。
    • GET /api/games/[gameId]: 特定ゲームの現在の状態を取得する (イベントを再生して構築)。
  4. UI実装:
    • ゲーム画面コンポーネント (app/(pages)/game/page.tsx など) を作成。
    • リプレイ画面コンポーネント (app/(pages)/replay/[gameId]/page.tsx など) を作成。
    • fetch を使用して API と連携する。
  5. イベントソーシング適用: API側で受け取った操作をイベントとして永続化し、状態取得リクエスト時にはイベントを読み込んで状態を再現するロジックを実装する。
riddle_tecriddle_tec


苦労したのはなんか画面のちらつきを抑えようとしたところ。

結局うまくいかなかった

このスクラップは4ヶ月前にクローズされました