Picasso Docs
Developers

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/
  • gameIdYYYYMMDD-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/:gameIdstate.json の部分更新(管理用)
POST/games/:gameId/slideロール紙を指定長さ(mm)だけ送る(非同期。202 Accepted)
GET/games/:gameId/slide/:jobIdロール紙スライドジョブステータス取得
POST/games/:gameId/captureWeb カメラで撮影し turn-N.png を保存
POST/games/:gameId/turn/advanceターンを次に進める
POST/games/:gameId/ai/analyzeユーザの画像解析 → 単語推測 → 勝敗判定
POST/games/:gameId/ai/nextAI で次の単語生成 → 勝敗判定
POST/games/:gameId/ai/plotAI 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 Createdstate.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 OKstate.json


PATCH /games/:gameId

説明: 管理用の部分更新(例: status, result をセット)。

リクエスト

{ "status": "finished", "result": "user-win" }

レスポンス: 200 OKstate.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 を保存、historyimage を更新。

リクエスト

なし(ボディ不要)

レスポンス 201 Createdstate.json


POST /games/:gameId/turn/advance

説明: ターンを進める。

リクエスト

なし(ボディ不要)

レスポンス 200 OKstate.json

内部の流れ:

  1. turn をインクリメントする。

  2. history[] に新たなエントリを追加する。( word, image は null)

    // 例
    
    "history": [
     ...,
     {
       "actor": "user",
       "word": null,
       "image": null
     }
    ]

POST /games/:gameId/ai/analyze

説明: 最新画像を AI 解析して単語を推測、勝敗を判定する。

リクエスト

なし(ボディ不要)

レスポンス 200 OKstate.json

内部の流れ:

  1. 最新 turn-N.png を解析して単語を解析する
  2. analyzedWord で終わればユーザ敗北 → result: "ai-win" , status: finished をセット

POST /games/:gameId/ai/next

説明: AI で次の単語を生成し、AI が生成した単語の勝敗判定

リクエスト

なし(ボディ不要)

レスポンス

201 Createdstate.json

内部の流れ:

  1. 直前のユーザの単語を元に新単語生成
  2. 生成された単語 が で終われば 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 OKstate.json

副作用: 実行中ジョブがあれば停止命令を試行(可能なら hardware 側の停止を呼び出す)。


5. (⚠️ 注意) state.json 更新の分担について

各 API は、 state.json のうちどのフィールドを更新するか明確に分担されています。 この分担に従うことで、サーバ・フロント間で常に整合性の取れた状態を維持します。

  • turn のインクリメント, history[] エントリの新規作成:

    • /turn/advance が行います。
  • history[] 中の word の更新:

    • ユーザのターンでは /ai/analyze が行います。
    • AI のターンでは /ai/next が行います。
  • history[] 中の image の更新:

    • /capture が行います。

ターン別の更新シーケンス

ユーザのターン

  1. ゲーム開始
    POST /games
+ {
+   "gameId": "20251007-192416",
+   "firstActor": "user",
+   "turn": 0,
+   "status": "playing",
+   "result": null,
+   "history": []
+ }
  1. turn++ 、新規エントリ追加
    POST /turn/advance
  {
    "gameId": "20251007-192416",
    "firstActor": "user",
-   "turn": 0,
+   "turn": 1,
    "status": "playing",
    "result": null,
    "history": [
+     {
+       "actor": "user",
+       "word": "null",
+       "image": "null"
+     }
    ]
  }
  1. 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"
      }
    ]
  }
  1. 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"
      }
    ]
  }
  1. AI のターンへ移行

AI のターン

  1. ゲーム開始
    POST /games
+ {
+   "gameId": "20251007-192416",
+   "firstActor": "ai",
+   "turn": 0,
+   "status": "playing",
+   "result": null,
+   "history": []
+ }
  1. turn++ 、新規エントリ追加
    POST /turn/advance
  {
    "gameId": "20251007-192416",
    "firstActor": "ai",
-   "turn": 0,
+   "turn": 1,
    "status": "playing",
    "result": null,
    "history": [
+     {
+       "actor": "ai",
+       "word": "null",
+       "image": "null"
+     }
    ]
  }
  1. word 追加(AI が思考)
    POST /ai/next
  {
    "gameId": "20251007-192416",
    "firstActor": "ai",
    "turn": 1,
    "status": "playing",
    "result": null,
    "history": [
      {
        "actor": "ai",
-       "word": null,
+       "word": "りんご",
        "image": null
      }
    ]
  }
  1. image 更新(プロッタ描画後)
    POST /capture
  {
    "gameId": "20251007-192416",
    "firstActor": "ai",
    "turn": 1,
    "status": "playing",
    "result": null,
    "history": [
      {
        "actor": "ai",
        "word": "りんご",
-       "image": null
+       "image": "turn-1.png"
      }
    ]
  }
  1. ユーザ のターンへ移行

まとめ(ターン進行の原則)

ターン手順API 呼び出し順序更新対象
ユーザ1️⃣ ターンを進める → 2️⃣ 画像を取得 → 3️⃣ 単語を解析/turn/advance/capture/ai/analyzeturn, image, word
AI1️⃣ ターンを進める → 2️⃣ 単語を生成 → 3️⃣ 画像を取得/turn/advance/ai/next/captureturn, 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;