API 仕様書
変更事項1
1のディレクトリ構成ですが、games/index.jsonを設けました。 このindex.jsonには、今までのゲームIDを格納します。 これにより、GET /games(過去ゲーム一覧取得)が不要になりました。
変更事項2
バックエンドには、games/ディレクトリを静的配信してもらって、state.jsonをフロントから参照するのがシンプルだと思われます。 それで、GET /games/:gameId(state.json取得)、PATCH /games/:gameId を削除しました。
当仕様書のファイル
1. ディレクトリ構成
project-root/
├─ backend/
│ ├─ src/
│ └─ games/
│ ├─ index.json
│ ├─ 20251007-192416/ # gameId = YYYYMMDD-HHMMSS
│ │ ├─ state.json
│ │ ├─ turn-1.png
│ │ ├─ turn-2.png
│ │ ├─ svg/
│ │ │ ├─ turn-2.svg
│ │ │ └─ turn-4.svg
│ │ └─ gcode/
│ │ ├─ turn-2.svg
│ │ └─ turn-4.svg
│ └─ 20251008-103029/
├─ frontend/
│ ├─ index.html
│ ├─ src/
│ └─ dist/ ← vite build の成果物
└─ package.jsongameIdはYYYYMMDD-HHMMSS形式。games/:gameId/state.jsonがそのゲームの SOT(source of truth)。- Express も Vite も
games/を静的ディレクトリとして配信する。
2. index.json フィールド
ゲーム ID 一覧を格納します。
例
[
"20251007-192416",
"20251008-103029",
"20251120-213030"
]これをフロントが直接 fetch して履歴一覧を表示。
3. state.json フォーマット
フィールド
-
gameId(string) -
firstActor("user" | "ai") -
turn(integer) — 最新ターン番号 -
status("playing" | "finished" | "aborted") -
result("ai-win" | "user-win" | "draw" | null) -
history(array of entries)-
entry:
actor("user" | "ai")word(string | null)image(string | null) — 相対パス(例:turn-3.png)
-
例
{
"gameId": "20251007-192416",
"firstActor": "user",
"turn": 3,
"status": "playing",
"result": null,
"history": [
{
"actor": "user",
"word": "りんご",
"image": "turn-1.png"
},
{
"actor": "ai",
"word": "ゴリラ",
"image": "turn-2.png"
},
{
"actor": "user",
"word": null,
"image": "turn-3.png"
}
]
}4. API 一覧(概要)
| メソッド | パス | 概要 |
|---|---|---|
| POST | /games | 新規ゲーム作成(gameId 発行、state.json 生成) |
| POST | /games/:gameId/capture | Web カメラで撮影し turn-N.png を保存 |
| POST | /games/:gameId/turn/advance | ターンを次に進める |
| POST | /games/:gameId/ai/analyze | ユーザの画像解析 → 単語推測 → 勝敗判定 |
| POST | /games/:gameId/ai/next | AI で次の単語生成 → 勝敗判定 |
| POST | /games/:gameId/ai/plot | AI SVG 生成 → G-code 変換 → プロッタ送信(非同期。202 Accepted) |
| GET | /games/:gameId/ai/plot/:jobId | 描画ジョブステータス取得 |
| POST | /games/:gameId/ai/hint | 単語のヒント生成 |
| POST | /games/:gameId/end | 強制終了(result = "draw" をセット) |
5. 各 API の詳細仕様
POST /games
説明: 新規ゲームを作成する。
state.json を初期値で生成。
リクエスト
{ "firstActor": "user" }初期state.json
{
"gameId": "20251120-213030", # YYYYMMDD-HHMMSS
"firstActor": "user", # user | ai
"turn": 0,
"status": "playing",
"result": null,
"history": []
}レスポンス
201 Created ・ state.json
POST /games/:gameId/capture
説明: カメラで撮影し turn-N.png を保存、history の image を更新。
リクエスト
なし(ボディ不要)
レスポンス
201 Created ・ state.json
POST /games/:gameId/turn/advance
説明: ターンを進める。
リクエスト
なし
レスポンス
200 OK ・ state.json
内部の流れ:
- turn をインクリメントする。
- history[] に新たなエントリを追加する。(
word,imageは null)
{
"gameId": "20251007-192416",
"turn": 1,
"history": [
{
"actor": "user",
"word": null,
"image": null
}
]
}
POST /games/:gameId/ai/analyze
説明: 最新画像を AI 解析して単語を推測、勝敗を判定する。
リクエスト
なし(ボディ不要)
レスポンス
200 OK ・ state.json
内部の流れ:
- 最新
turn-N.pngを解析して単語を解析する analyzedWordがんで終わればユーザ敗北 →result: "ai-win",status: finishedをセット
POST /games/:gameId/ai/next
説明: AI で次の単語を生成し、AI が生成した単語の勝敗判定
リクエスト
なし(ボディ不要)
レスポンス
201 Created ・ state.json
内部の流れ:
- 直前のユーザの単語を元に新単語生成
- 生成された単語 が
んで終われば AI 敗北 →result: "user-win",status: finishedをセット
POST /games/:gameId/ai/plot
説明: 指定単語を SVG 化 →G-code 化 → プロッタへ送信して描画を開始する。処理は非同期。
リクエスト
{ "word": "ごりら" }レスポンス
202 Accepted
{
"status": "accepted",
"jobId": "job-20251007-192416-plot-1",
"message": "Plotting started."
}副作用(非同期ワーカー)
- SVG 保存 → G-code 保存 → Grbl へ送信
GET /games/:gameId/ai/plot/:jobId
説明: 描画ジョブのステータス取得
レスポンス
{
"jobId": "job-20251007-192416-plot-1",
"status": "plotting"
}statusの内容
svg_generating : svg生成中
plotting G-code 変換 → プロッタ描画中
done : 全工程が完了
error : いずれかの工程で失敗
POST /games/:gameId/ai/hint
説明: 単語のヒントを生成して返す
リクエスト
{ "word": "たんぽぽ" }レスポンス
{ "hint": "黄色に咲く花で、華やかです。" }POST /games/:gameId/end
説明: 強制終了(UI の「ゲーム終了」ボタン)。result を "draw" にして status を "finished" に更新する。
リクエスト: 省略可(任意で reason を送れる)
レスポンス
200 OK ・ state.json
副作用: 実行中ジョブがあれば停止命令を試行(可能なら hardware 側の停止を呼び出す)。
6. (⚠️ 注意) state.json 更新の分担について
各 API は、 state.json のうちどのフィールドを更新するか明確に分担されています。
この分担に従うことで、サーバ・フロント間で常に整合性の取れた状態を維持します。
-
turnのインクリメント,history[]エントリの新規作成:/turn/advanceが行います。
-
history[]中のwordの更新:- ユーザのターンでは
/ai/analyzeが行います。 - AI のターンでは
/ai/nextが行います。
- ユーザのターンでは
-
history[]中のimageの更新:/captureが行います。
ターン別の更新シーケンス
ユーザのターン
- ゲーム開始
POST /games
+ {
+ "gameId": "20251007-192416",
+ "firstActor": "user",
+ "turn": 0,
+ "status": "playing",
+ "result": null,
+ "history": []
+ }turn++、新規エントリ追加
POST /turn/advance
{
"gameId": "20251007-192416",
"firstActor": "user",
- "turn": 0,
+ "turn": 1,
"status": "playing",
"result": null,
"history": [
+ {
+ "actor": "user",
+ "word": null,
+ "image": null
+ }
]
}image更新
POST /capture
{
"gameId": "20251007-192416",
"firstActor": "user",
"turn": 1,
"status": "playing",
"result": null,
"history": [
{
"actor": "user",
"word": null,
- "image": null
+ "image": "turn-1.png"
}
]
}word追加
POST /ai/analyze
{
"gameId": "20251007-192416",
"firstActor": "user",
"turn": 1,
"status": "playing",
"result": null,
"history": [
{
"actor": "user",
- "word": null,
+ "word": "りんご",
"image": "turn-1.png"
}
]
}- AI のターンへ移行
AI のターン
- ゲーム開始
POST /games
+ {
+ "gameId": "20251007-192416",
+ "firstActor": "ai",
+ "turn": 0,
+ "status": "playing",
+ "result": null,
+ "history": []
+ }turn++、新規エントリ追加
POST /turn/advance
{
"gameId": "20251007-192416",
"firstActor": "ai",
- "turn": 0,
+ "turn": 1,
"status": "playing",
"result": null,
"history": [
+ {
+ "actor": "ai",
+ "word": null,
+ "image": null
+ }
]
}word追加(AI が思考)
POST /ai/next
{
"gameId": "20251007-192416",
"firstActor": "ai",
"turn": 1,
"status": "playing",
"result": null,
"history": [
{
"actor": "ai",
- "word": null,
+ "word": "りんご",
"image": null
}
]
}image更新(プロッタ描画後)
POST /capture
{
"gameId": "20251007-192416",
"firstActor": "ai",
"turn": 1,
"status": "playing",
"result": null,
"history": [
{
"actor": "ai",
"word": "りんご",
- "image": null
+ "image": "turn-1.png"
}
]
}- ユーザ のターンへ移行
まとめ(ターン進行の原則)
| ターン | 手順 | API 呼び出し順序 | 更新対象 |
|---|---|---|---|
| ユーザ | 1️⃣ ターンを進める → 2️⃣ 画像を取得 → 3️⃣ 単語を解析 | /turn/advance → /capture → /ai/analyze | turn, image, word |
| AI | 1️⃣ ターンを進める → 2️⃣ 単語を生成 → 3️⃣ 画像を取得 | /turn/advance → /ai/next → /capture | turn, word, image |
7. TypeScript 型定義
以下は実装時に使える TypeScript の型定義です。
// types.ts
// =================
// 基本形
// =================
export type Actor = 'user' | 'ai';
export type GameStatus = 'playing' | 'finished' | 'aborted';
export type GameResult = 'ai-win' | 'user-win' | 'draw' | null;
export interface HistoryEntry {
actor: Actor;
word: string | null;
image: string | null; // 相対パス e.g. "turn-3.png"
}
export interface GameState {
gameId: string; // YYYYMMDD-HHMMSS
firstActor: Actor;
turn: number; // 最新ターン番号
status: GameStatus;
result: GameResult; // "ai-win" | "user-win" | "draw" | null
history: HistoryEntry[];
}
// =================
// API
// =================
// POST /games
export interface CreateGameRequest {
firstActor: Actor;
}
export type CreateGameResponse = GameState;
// POST /games/{gameId}/capture
export type CaptureResponse = GameState;
// POST /games/{gameId}/turn/advance
export type AdvanceTurnResponse = GameState;
// POST /games/{gameId}/ai/analyze
export type AnalyzeResponse = GameState;
// POST /games/{gameId}/ai/next
export type NextResponse = GameState;
// POST /games/{gameId}/ai/plot
export interface PlotRequest {
word: string;
}
export interface PlotResponse {
status: 'accepted' | 'error';
jobId?: string;
message?: string;
}
// GET /games/{gameId}/ai/plot/{jobId}
export type PlotJobStatus = 'svg_generating' | 'plotting' | 'done' | 'error';
export interface PlotJobResponse {
jobId: string;
status: PlotJobStatus;
}
// POST /games/{gameId}/ai/hint
export interface HintRequest {
word: string;
}
export interface HintResponse {
hint: string;
}
// POST /games/{gameId}/end
export type EndResponse = GameState;