REST API 仕様書
当仕様書のファイル(AI に投げる用)
以下をコピペすれば、API 設計を AI に投げる際に便利です。
この仕様書のファイル
👈 Click This!!
0. 設計思想
本 API は「state.json を唯一の真実(Single Source of Truth)とする」ことを基本方針とします。 すべての状態はこのファイルを通じて管理・共有され、サーバ・クライアント間で一貫して扱われます。 なお、展示環境向けのローカル操作のため、API認証は不要です。
-
状態の一元管理
- ゲームの状態はすべて
state.json
に集約し、サーバが正として扱う。 - 更新系 API(
capture
,analyze
,next
,advance
,end
など)は、更新後のstate.json
全体を返す。
- ゲームの状態はすべて
-
フロントエンドとの関係
- フロントは
state.json
のコピーを保持し、UI 表示用に随時更新する。 - API 呼び出し後、返却された最新の
state.json
を反映して同期を保ち、サーバと同一の状態を再現する。
- フロントは
1. ディレクトリ構成
/app-root
├─ games/
│ ├─ 20251007-192416/ # gameId = YYYYMMDD-HHMMSS
│ │ ├─ state.json
│ │ ├─ turn-1.png
│ │ ├─ turn-2.png
│ │ ├─ svg/
│ │ │ ├─ turn-2.svg
│ │ │ └─ turn-4.svg
│ │ └─ gcode/
│ │ ├─ turn-2.gcode
│ │ └─ turn-4.gcode
│ └─ 20251008-103029/
│ └─ ...
├─ uploads/ # (任意)一時保存用
├─ logs/
└─ config/
gameId
はYYYYMMDD-HHMMSS
形式。games/:gameId/state.json
がそのゲームの SOT(source of truth)。
2. state.json フォーマット
フィールド
-
gameId
(string) -
firstActor
("user" | "ai") -
turn
(integer) — 最新ターン番号 -
status
("created" | "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"
}
]
}
3. API 一覧(概要)
メソッド | パス | 概要 |
---|---|---|
POST | /games | 新規ゲーム作成(gameId 発行、state.json 生成) |
GET | /games | 過去ゲーム一覧取得(gameId, startedAt, result, summary) |
GET | /games/:gameId | ゲーム状態取得(state.json を返す) |
PATCH | /games/:gameId | state.json の部分更新(管理用) |
POST | /games/:gameId/slide | ロール紙を指定長さ(mm)だけ送る(非同期。202 Accepted) |
GET | /games/:gameId/slide/:jobId | ロール紙スライドジョブステータス取得 |
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" をセット) |
4. 各 API の詳細仕様
POST /games
説明: 新規ゲームを作成する。
リクエスト
{ "firstActor": "user" }
レスポンス
201 Created
・ state.json
副作用: games/:gameId
ディレクトリと初期 state.json
を作成。
GET /games
説明: すべての games/*/state.json
を参照して簡易一覧を返す。
レスポンス
200 OK
[
{ "gameId": "20251007-192416", "result": "draw" },
{ "gameId": "20251008-103029", "result": "ai-win" }
]
GET /games/:gameId
説明: state.json
を返す。
レスポンス: 200 OK
・ state.json
PATCH /games/:gameId
説明: 管理用の部分更新(例: status
, result
をセット)。
リクエスト
{ "status": "finished", "result": "user-win" }
レスポンス: 200 OK
・ state.json
POST /games/:gameId/slide
説明: ロール紙を指定長さだけ送る。処理は非同期。
リクエスト
{ "length": 100 }
レスポンス
202 Accepted
{
"status": "accepted",
"jobId": "job-20251007-192416-slide-100",
"message": "Sliding started."
}
注意: ハードウェア通信エラーは 503
を返す。
GET /games/:gameId/slide/:jobId
説明: スライドジョブのステータス取得
レスポンス
{
"jobId": "job-20251007-192416-slide-100",
"status": "sliding"
}
statusの内容
pending
: 待ち
sliding
ロール紙スライド中
done
: 全工程が完了
error
: いずれかの工程で失敗
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)// 例 "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 側の停止を呼び出す)。
5. (⚠️ 注意) 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 |
6. TypeScript 型定義
以下は実装時に使える TypeScript の型定義です。
// types.ts
// =================
// 基本形
// =================
export type Actor = 'user' | 'ai';
export type GameStatus = 'created' | '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;
// GET /games (一覧の各要素)
export interface GamesListItem {
gameId: string;
result: GameResult;
}
// GET /games/:gameId
export type GetGameStateResponse = GameState;
// PATCH /games/:gameId
export interface PatchGameRequest {
status?: GameStatus;
result?: GameResult;
// 他のフィールドも部分更新可能にする場合は追加
}
export type PatchGameResponse = GameState;
// POST /games/{gameId}/slide
export interface SlideRequest {
length: number; // mm
}
export interface SlideResponse {
status: 'accepted' | 'error';
jobId?: string;
message?: string;
}
// GET /games/{gameId}/slide/{jobId}
export type SlideJobStatus = 'pending' | 'sliding' | 'done' | 'error';
export interface SlideJobResponse {
jobId: string;
status: SlideJobStatus;
}
// 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;