From fa86f555af4f760f019d7ed7ef82663f3005144d Mon Sep 17 00:00:00 2001 From: kontei Date: Sun, 15 Feb 2026 14:53:03 +0900 Subject: [PATCH] =?UTF-8?q?=E3=83=90=E3=83=83=E3=82=AF=E3=82=A8=E3=83=B3?= =?UTF-8?q?=E3=83=89API?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .vscode/settings.json | 3 + README.md | 224 +++++++++++++++ __pycache__/main.cpython-314.pyc | Bin 0 -> 13153 bytes bot_research.py | 49 ++++ debug_dip.py | 33 +++ debug_render.py | 70 +++++ main.py | 230 +++++++++++++++ requirements.txt | 2 + research_dip.py | 23 ++ test_api.py | 83 ++++++ test_nmr.py | 86 ++++++ test_process.json | 74 +++++ test_render.svg | 467 +++++++++++++++++++++++++++++++ test_render_f1901.svg | 467 +++++++++++++++++++++++++++++++ test_state.json | 71 +++++ test_w1901a.json | 71 +++++ verify_possible_orders.py | 80 ++++++ verify_process.py | 184 ++++++++++++ 18 files changed, 2217 insertions(+) create mode 100644 .vscode/settings.json create mode 100644 README.md create mode 100644 __pycache__/main.cpython-314.pyc create mode 100644 bot_research.py create mode 100644 debug_dip.py create mode 100644 debug_render.py create mode 100644 main.py create mode 100644 requirements.txt create mode 100644 research_dip.py create mode 100644 test_api.py create mode 100644 test_nmr.py create mode 100644 test_process.json create mode 100644 test_render.svg create mode 100644 test_render_f1901.svg create mode 100644 test_state.json create mode 100644 test_w1901a.json create mode 100644 verify_possible_orders.py create mode 100644 verify_process.py diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..5480842 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "kiroAgent.configureMCP": "Disabled" +} \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..a1faf35 --- /dev/null +++ b/README.md @@ -0,0 +1,224 @@ +# Diplomacy Calculation Engine API + +ディプロマシー(Diplomacy)ボードゲームの計算エンジンとして機能する、ステートレスなFastAPIアプリケーションです。 +`diplomacy`ライブラリの計算ロジックをウェブAPIとして提供します。フロントエンドアプリケーション等から利用されることを想定しています。 + +## 主な機能 + +* **ステートレスな計算**: ゲームの状態(JSON)と命令を送り、次の状態を取得可能。 +* **命令の解決 (Processing)**: フェイズを進行させ、命令を解決します。 +* **命令の検証 (Validation)**: 指定された命令がルール上有効かどうかをチェック。 +* **実行可能な命令の取得**: 現在の盤面で各ユニットができる全命令のリストを取得。 +* **可視化 (Rendering)**: 現在の盤面や入力中の命令を反映したSVG画像を生成。 + +## 開発環境のセットアップ + +```bash +# 仮想環境の作成と有効化 +python -m venv .venv +source .venv/bin/activate # Linux/macOS + +# 依存ライブラリのインストール +pip install -r requirements.txt +pip install requests # テスト用 +``` + +## 起動方法 + +### 推奨: Pythonコマンド + +```bash +python main.py +``` + +### Option: uvicornコマンド + +```bash +# main.pyがルートにあるため main:app と指定します +uvicorn main:app --host 127.0.0.1 --port 8000 +``` + +起動後、Swagger UIが [http://127.0.0.1:8000/docs](http://127.0.0.1:8000/docs) で利用可能です。 + +--- + +## API エンドポイント仕様 + +### 1. マップ情報の取得 + +#### マップ一覧の取得 + +* **URL**: `/maps` +* **Method**: `GET` +* **Description**: 利用可能なマップ名の一覧を返します。 + +#### マップ詳細の取得 + +* **URL**: `/maps/{map_name}` +* **Method**: `GET` +* **Description**: 指定したマップのトポロジー情報(地域、隣接関係など)を返します。 +* **Example**: `/maps/standard` + +### 2. 計算・フェイズ進行 + +#### ゲームの進行 (Process) + +現在の状態と命令を受け取り、判定を行い、次のフェイズの状態を返します。 + +* **URL**: `/calculate/process` +* **Method**: `POST` +* **Request Body**: + + ```json + { + "game_state": { ... }, // diplomacyライブラリの Game.to_dict() 形式 + "orders": { + "FRANCE": ["A MAR - SPA", "A PAR - BUR"], + "ENGLAND": ["F LON - ENG"] + } + } + ``` + +* **Response**: + + ```json + { + "game_state": { ... } // 更新された新しいゲーム状態 + } + ``` + +#### 命令の検証 (Validate) + +指定された命令が現在の局面で有効かどうかを検証します(フェイズは進みません)。 + +* **URL**: `/calculate/validate` +* **Method**: `POST` +* **Request Body**: `process` と同様(`game_state` と `orders`) +* **Response**: + + ```json + { + "validated_orders": { + "FRANCE": ["A MAR - SPA"] // 受け付けられた命令 + }, + "errors": [] // エラーがあればここにリストされます + } + ``` + +#### 実行可能な命令の取得 (Possible Orders) + +現在の局面で実行可能なすべての命令のリストを取得します。 + +* **URL**: `/calculate/possible-orders` +* **Method**: `POST` +* **Query Param**: + * `power_name` (Optional) - 特定の国の命令のみ取得する場合に指定。 + * `by_power` (Optional) - `true`を指定すると、結果を国ごとにグループ化して返します。 +* **Request Body**: + + ```json + { + "game_state": { ... } // diplomacyライブラリの Game.to_dict() 形式 + } + ``` + +* **Response**: + + ```json + { + "possible_orders": { + "ADR": [], // ユニットがいない地域 + "ANK": [ ... ], // ユニットがいる地域の命令リスト + ... + } + } + ``` + + ※ `by_power=true` 指定時は、以下のように国名がキーとなります: + + ```json + { + "possible_orders": { + "FRANCE": { + "PAR": [ ... ], + "MAR": [ ... ] + }, + "ENGLAND": { ... } + } + } + ``` + + ※ `power_name` 指定時は、その国のユニットが存在する地域のみが含まれます。 + +#### 命令の自動生成 (Auto Orders) + +指定した国に対して、ランダムに有効な命令を生成します。 + +* **URL**: `/calculate/auto-orders` +* **Method**: `POST` +* **Query Param**: `power_name` (Required) +* **Request Body**: + + ```json + { + "game_state": { ... } + } + ``` + +* **Response**: + + ```json + { + "orders": ["A PAR - BUR", "F BRE H", ...] + } + ``` + +#### ゲームの強制終了 (Draw) + +ゲームの状態を手動で「引き分け (Draw)」に変更し、フェイズを `COMPLETED` にします。 + +* **URL**: `/game/draw` +* **Method**: `POST` +* **Request Body**: + + ```json + { + "game_state": { ... }, // Game.to_dict() 形式 + "winners": ["FRANCE", "GERMANY"] // (Optional) 特定の勝者を指定する場合 + } + ``` + +* **Response**: + + ```json + { + "game_state": { ... } // COMPLETED状態のゲーム + } + ``` + +### 3. 可視化 (Rendering) + +#### マップ画像の生成 + +現在の状態(および入力中の命令)を反映したSVG画像を生成します。 + +* **URL**: `/render` +* **Method**: `POST` +* **Request Body**: + + ```json + { + "game_state": { ... }, + "orders": { ... }, // Optional: 入力中の命令(矢印で描画されます) + "incl_orders": true, // Optional: 命令を描画するか (default: true) + "incl_abbrev": true // Optional: 地名の略称を表示するか (default: true) + } + ``` + +* **Response**: `image/svg+xml` (SVG画像データ) + +## プロジェクト構造 + +* `main.py`: APIサーバー実装 +* `test_api.py`: 動作確認用スクリプト +* `requirements.txt`: 依存パッケージ diff --git a/__pycache__/main.cpython-314.pyc b/__pycache__/main.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..9b00dafe5fb1ed8c6d34fcb3d9c10997a2c84492 GIT binary patch literal 13153 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 List of Orders + +class RenderRequest(BaseModel): + """Request for rendering the game map.""" + game_state: Dict[str, Any] + orders: Optional[Dict[str, List[str]]] = None + incl_orders: bool = True + incl_abbrev: bool = True + +class DrawRequest(BaseModel): + """Request to force a draw in the game.""" + game_state: Dict[str, Any] + winners: Optional[List[str]] = None + +# --- Helper Functions --- + +def load_game(state_dict: Dict[str, Any]) -> Game: + """Loads a Game object from a dictionary.""" + try: + game = Game() + game.set_state(state_dict) + return game + except Exception as e: + raise HTTPException(status_code=400, detail=f"Failed to load game state: {str(e)}") + +@app.get("/game/initial-state") +def get_initial_state(map_name: str = "standard"): + """Returns the initial game state for a given map.""" + try: + game = Game(map_name=map_name) + return {"game_state": game.get_state()} + except Exception as e: + raise HTTPException(status_code=400, detail=f"Failed to create initial state for map '{map_name}': {str(e)}") + +@app.get("/debug_heartbeat") +def debug_heartbeat(): + """Simple heartbeat to check if server code is updated.""" + return {"status": "ok", "version": "debug_v1"} + +# --- Endpoints --- + +@app.get("/maps") +def list_maps(): + """List available maps (standard library maps).""" + # Simply return common ones for now, can be expanded + return {"maps": ["standard"]} + +@app.get("/maps/{map_name}") +def get_map_details(map_name: str): + """Get topology and details of a specific map.""" + try: + d_map = Map(map_name) + return { + "name": map_name, + "provinces": d_map.locs, + "adjacencies": {loc: d_map.abut_list(loc) for loc in d_map.locs}, + "powers": d_map.powers, + "centers": d_map.scs, + "homes": d_map.homes + } + except Exception as e: + raise HTTPException(status_code=404, detail=f"Map '{map_name}' not found: {str(e)}") + +@app.post("/calculate/process") +def process_phase(request: OrderRequest): + """Resolves orders and advances to the next phase.""" + game = load_game(request.game_state) + + # Set orders for each power + for power, orders in request.orders.items(): + if power in game.powers: + game.set_orders(power, orders) + + # Process the game + game.process() + + return {"game_state": game.get_state()} + +@app.post("/calculate/validate") +def validate_orders(request: OrderRequest): + """Validates orders without advancing the phase.""" + game = load_game(request.game_state) + + results = {} + for power, orders in request.orders.items(): + if power in game.powers: + game.set_orders(power, orders) + # diplomacy library validates during set_orders and process or via specific methods + # Here we can check if orders were actually accepted or get status + results[power] = game.get_orders(power) + + return {"validated_orders": results, "errors": game.error} + +@app.post("/game/draw") +def draw_game(request: DrawRequest): + """Forces the game to a draw status.""" + game = load_game(request.game_state) + + # Process the draw + game.draw(winners=request.winners) + + return {"game_state": game.get_state()} + +@app.post("/calculate/possible-orders") +def get_possible_orders(request: GameState, power_name: Optional[str] = None, by_power: bool = False): + """Get all possible valid orders for the current state.""" + game = load_game(request.game_state) + + all_possibilities = game.get_all_possible_orders() + + if by_power: + results = {} + for power in game.powers: + # Get unit locations for this power + power_units = game.get_units(power) + power_locs = [u.split(' ')[1] for u in power_units] + + # For adjustment phases, also include build locations (homes where units can be built) + if 'ADJUSTMENT' in game.phase: + # Get build information for this power + builds_info = game.get_state().get('builds', {}).get(power, {}) + build_homes = builds_info.get('homes', []) + power_locs.extend(build_homes) + + results[power] = {loc: orders for loc, orders in all_possibilities.items() if loc in power_locs} + + if power_name: + power_name = power_name.upper() + if power_name not in results: + raise HTTPException(status_code=404, detail=f"Power '{power_name}' not found") + return {"power": power_name, "possible_orders": results[power_name]} + + return {"possible_orders": results} + + if power_name: + power_name = power_name.upper() + if power_name not in game.powers: + raise HTTPException(status_code=404, detail=f"Power '{power_name}' not found") + + # Get unit locations for this power + power_units = game.get_units(power_name) # returns list like ['A PAR', 'F BRE'] + power_locs = [u.split(' ')[1] for u in power_units] + + # For adjustment phases, also include build locations + if 'ADJUSTMENT' in game.phase: + builds_info = game.get_state().get('builds', {}).get(power_name, {}) + build_homes = builds_info.get('homes', []) + power_locs.extend(build_homes) + + filtered = {loc: orders for loc, orders in all_possibilities.items() if loc in power_locs} + return {"power": power_name, "possible_orders": filtered} + + return {"possible_orders": all_possibilities} + +@app.post("/calculate/auto-orders") +def auto_orders(request: GameState, power_name: str): + """Generates random valid orders for a specific power.""" + game = load_game(request.game_state) + power_name = power_name.upper() + + if power_name not in game.powers: + raise HTTPException(status_code=404, detail=f"Power '{power_name}' not found") + + # Check for units + power_units = game.get_units(power_name) + if not power_units: + return {"orders": []} + + possible_orders = game.get_all_possible_orders() + orders_to_submit = [] + + for unit in power_units: + loc = unit.split(' ')[1] + if loc in possible_orders: + unit_orders = possible_orders[loc] + if unit_orders: + orders_to_submit.append(random.choice(unit_orders)) + + return {"orders": orders_to_submit} + +@app.post("/render") +def render_map(request: RenderRequest): + """Renders the game map as an SVG.""" + game = load_game(request.game_state) + + # If orders are provided, set them temporarily for rendering + if request.orders: + for power, orders in request.orders.items(): + if power in game.powers: + game.set_orders(power, orders) + + svg_content = game.render( + incl_orders=request.incl_orders, + incl_abbrev=request.incl_abbrev, + output_format='svg' + ) + + response = Response(content=svg_content, media_type="image/svg+xml") + # Debug info + try: + all_units = game.get_units() + unit_count = sum(len(u) for u in all_units.values()) if all_units else 0 + response.headers["X-Debug-Unit-Count"] = str(unit_count) + # Austria units as example + response.headers["X-Debug-Units-Austria"] = str(game.get_units("AUSTRIA")) + except Exception as e: + response.headers["X-Debug-Error"] = str(e) + + return response + +if __name__ == "__main__": + import uvicorn + uvicorn.run(app, host="0.0.0.0", port=8000) diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..fa965c1 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,2 @@ +fastapi[all] +diplomacy diff --git a/research_dip.py b/research_dip.py new file mode 100644 index 0000000..3950184 --- /dev/null +++ b/research_dip.py @@ -0,0 +1,23 @@ +from diplomacy import Game + +def research(): + game = Game() + print(f"Current Phase: {game.phase}") + print(f"Powers: {list(game.powers.keys())}") + + # Check powers and units + for power_name, power in game.powers.items(): + print(f"Power: {power_name}, Centers: {power.centers}, Units: {power.units}") + + # Check serialization + # Game objects have a to_dict method or similar? + # Let's inspect the object + print("\nAttributes of Game object:") + print([attr for attr in dir(game) if not attr.startswith('_')]) + + # Example of setting orders + # game.set_orders('FRANCE', ['A MAR H', 'A PAR - BUR', 'F BRE - MAO']) + # game.process() + +if __name__ == "__main__": + research() diff --git a/test_api.py b/test_api.py new file mode 100644 index 0000000..369a13e --- /dev/null +++ b/test_api.py @@ -0,0 +1,83 @@ +import requests +import json +from diplomacy import Game + +# API URL +BASE_URL = "http://127.0.0.1:8000" + +def test_api(): + # 1. Test GET /maps + print("Testing /maps...") + resp = requests.get(f"{BASE_URL}/maps") + print(resp.json()) + + # 2. Test POST /calculate/process (Spring 1901 -> Fall 1901) + print("\nTesting /calculate/process...") + game = Game() + initial_state = game.to_dict() + + payload = { + "game_state": initial_state, + "orders": { + "FRANCE": ["A MAR H", "A PAR - BUR", "F BRE - MAO"], + "ENGLAND": ["F EDI - NTH", "F LON - ENG", "A LVP - WAL"] + } + } + + resp = requests.post(f"{BASE_URL}/calculate/process", json=payload) + result = resp.json() + new_phase = result["game_state"]["phase"] + print(f"New Phase: {new_phase}") + assert "FALL 1901" in new_phase + + # 3. Test POST /render + print("\nTesting /render...") + payload = { + "game_state": initial_state, + "orders": { + "FRANCE": ["A MAR - SPA", "A PAR - BUR", "F BRE - MAO"] + }, + "incl_orders": True + } + resp = requests.post(f"{BASE_URL}/render", json=payload) + print(f"Render Status: {resp.status_code}") + print(f"Content Type: {resp.headers.get('Content-Type')}") + assert resp.status_code == 200 + assert "image/svg+xml" in resp.headers.get('Content-Type') + + # Save SVG to check manually if possible + with open("test_render.svg", "wb") as f: + f.write(resp.content) + print("Saved test_render.svg") + # 4. Test POST /calculate/possible-orders + print("\nTesting /calculate/possible-orders...") + payload = { + "game_state": initial_state + } + resp = requests.post(f"{BASE_URL}/calculate/possible-orders", json=payload) + result = resp.json() + print(f"Possible Orders (Keys): {list(result['possible_orders'].keys())[:5]}") + assert "BUD" in result['possible_orders'] + + # 5. Test initial state + print("\nTesting /game/initial-state...") + resp = requests.get(f"{BASE_URL}/game/initial-state") + result = resp.json() + assert "game_state" in result + print(f"Initial Phase: {result['game_state']['phase']}") + + # 6. Test auto orders + print("\nTesting /calculate/auto-orders...") + payload = { + "game_state": initial_state + } + resp = requests.post(f"{BASE_URL}/calculate/auto-orders?power_name=FRANCE", json=payload) + assert resp.status_code == 200 + result = resp.json() + print(f"Auto Orders for FRANCE: {result['orders']}") + assert len(result['orders']) > 0 + # Basic check to see if orders look valid (simple heuristic) + assert any("A PAR" in o or "A MAR" in o or "F BRE" in o for o in result['orders']) + +if __name__ == "__main__": + test_api() diff --git a/test_nmr.py b/test_nmr.py new file mode 100644 index 0000000..06d04fc --- /dev/null +++ b/test_nmr.py @@ -0,0 +1,86 @@ +from diplomacy import Game + +def test_nmr(): + game = Game() + # France does nothing + # England moves + game.set_orders("ENGLAND", ["F LON - ENG", "A LVP - WAL", "F EDI - NTH"]) + + print("Processing Spring 1901...") + game.process() + + # Check France + france = game.get_power("FRANCE") + print(f"France Civil Disorder Status: {france.civil_disorder}") + print(f"France Units: {france.units}") + # France orders in the previous phase (Spring 1901) + # The game object stores history? + # game.get_orders(power_name) gets *current* phase orders (empty) + + # We can inspect the map or units to see if they moved? + # But France didn't move. + + # Let's verify they are still at starting positions + print(f"France Units after process: {france.units}") + assert 'A PAR' in france.units + assert 'F BRE' in france.units + assert 'A MAR' in france.units + + # Also Check if we can find the 'H' orders in history if available + # game.order_history is a thing? + # From dir(game) in previous turn: 'order_history' + + # keys of order_history are timestamps or phase names? + # Let's print keys + # print(f"Order History Keys: {game.order_history.keys()}") + # It might be keyed by phase name 'SPRING 1901 MOVEMENT' + + + # Test 2: Auto-dsiband in Adjustment Phase + print("\nTesting Auto-Disband...") + # Create a situation where France loses a center and must disband + # We can force set units and centers + g2 = Game() + g2.set_units("FRANCE", ["A BUR", "A PAR"]) # 2 units + g2.set_centers("FRANCE", ["PAR"]) # 1 center (needs to remove 1) + # Phase needs to be adjustment? Or just process? + # Game starts at Spring 1901. We need to set phase to Winter 1901? + # Or just manipulate state to be in adjustment? + + # Easiest way: force phase + from diplomacy.utils.game_phase_data import GamePhaseData + g2.phase = "WINTER 1901 ADJUSTMENTS" + + # We must ensure the game knows it's adjustment phase data structure + # Actually just processing might not work if we brute force phase string? + # Better to correct way: + # Let's just use a map where we can skip to adjustments or manually set it up correct. + # brute forcing might break internal state. + + # Alternative: + # 1. France holds + # 2. Enemy takes a center + # 3. Fall ends + # 4. France must disband + + # Let's try brute forcing phase + clear cache if possible, but let's try the cleaner way + # Set Units/Centers then process? + # The game checks phase validity. + + # Let's try: + g3 = Game() + g3.set_units("FRANCE", ["A PAR", "A MAR"]) + g3.set_centers("FRANCE", ["PAR"]) # Lost MAR + g3.phase = "WINTER 1901 ADJUSTMENTS" + # We might need to ensure 'A MAR' is considered a unit. + + print("Processing Adjustment Phase (No Orders for France)...") + g3.process() + print(f"France Units after adjustment: {g3.get_units('FRANCE')}") + # A MAR is further from PAR than A PAR? + # Distance PAR-PAR = 0. + # Distance MAR-PAR = 1. + # So A MAR should be disbanded. + +if __name__ == "__main__": + test_nmr() diff --git a/test_process.json b/test_process.json new file mode 100644 index 0000000..186d172 --- /dev/null +++ b/test_process.json @@ -0,0 +1,74 @@ +{ + "game_state": { + "timestamp": 1770652252578290, + "zobrist_hash": "853113961011677805", + "note": "", + "name": "F1901M", + "units": { + "AUSTRIA": ["A BUD", "A VIE", "F TRI"], + "ENGLAND": ["F EDI", "F LON", "A LVP"], + "FRANCE": ["A MAR", "A PAR", "F PIC"], + "GERMANY": ["F KIE", "A BER", "A MUN"], + "ITALY": ["F NAP", "A ROM", "A VEN"], + "RUSSIA": ["A WAR", "A MOS", "F SEV", "F STP/SC"], + "TURKEY": ["F ANK", "A CON", "A SMY"] + }, + "retreats": { + "AUSTRIA": {}, + "ENGLAND": {}, + "FRANCE": {}, + "GERMANY": {}, + "ITALY": {}, + "RUSSIA": {}, + "TURKEY": {} + }, + "centers": { + "AUSTRIA": ["BUD", "TRI", "VIE"], + "ENGLAND": ["EDI", "LON", "LVP"], + "FRANCE": ["BRE", "MAR", "PAR"], + "GERMANY": ["BER", "KIE", "MUN"], + "ITALY": ["NAP", "ROM", "VEN"], + "RUSSIA": ["MOS", "SEV", "STP", "WAR"], + "TURKEY": ["ANK", "CON", "SMY"] + }, + "homes": { + "AUSTRIA": ["BUD", "TRI", "VIE"], + "ENGLAND": ["EDI", "LON", "LVP"], + "FRANCE": ["BRE", "MAR", "PAR"], + "GERMANY": ["BER", "KIE", "MUN"], + "ITALY": ["NAP", "ROM", "VEN"], + "RUSSIA": ["MOS", "SEV", "STP", "WAR"], + "TURKEY": ["ANK", "CON", "SMY"] + }, + "influence": { + "AUSTRIA": ["BUD", "VIE", "TRI"], + "ENGLAND": ["EDI", "LON", "LVP"], + "FRANCE": ["BRE", "MAR", "PAR", "PIC"], + "GERMANY": ["KIE", "BER", "MUN"], + "ITALY": ["NAP", "ROM", "VEN"], + "RUSSIA": ["WAR", "MOS", "SEV", "STP"], + "TURKEY": ["ANK", "CON", "SMY"] + }, + "civil_disorder": { + "AUSTRIA": 0, + "ENGLAND": 0, + "FRANCE": 0, + "GERMANY": 0, + "ITALY": 0, + "RUSSIA": 0, + "TURKEY": 0 + }, + "builds": { + "AUSTRIA": {"count": 0, "homes": []}, + "ENGLAND": {"count": 0, "homes": []}, + "FRANCE": {"count": 0, "homes": []}, + "GERMANY": {"count": 0, "homes": []}, + "ITALY": {"count": 0, "homes": []}, + "RUSSIA": {"count": 0, "homes": []}, + "TURKEY": {"count": 0, "homes": []} + } + }, + "orders": { + "FRANCE": ["A MAR - BUR", "A PAR H", "F PIC - BEL"] + } +} diff --git a/test_render.svg b/test_render.svg new file mode 100644 index 0000000..47e3f8a --- /dev/null +++ b/test_render.svg @@ -0,0 +1,467 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + SWI + ADR + AEG + ALB + ANK + APU + ARM + BAL + BAR + BEL + BER + BLA + BOH + BRE + BUD + BUL + BUR + CLY + CON + DEN + EAS + EDI + ENG + FIN + GAL + GAS + GRE + BOT + LYO + HEL + HOL + ION + IRI + KIE + LON + LVN + LVP + MAR + MAO + MOS + MUN + NAF + NAP + NAO + NTH + NWY + NWG + PAR + PIC + PIE + POR + PRU + ROM + RUH + RUM + SER + SEV + SIL + SKA + SMY + SPA + STP + SWE + SYR + TRI + TUN + TUS + TYR + TYS + UKR + VEN + VIE + WAL + WAR + WES + YOR + + + + + RUS: 4 AUS: 3 ENG: 3 FRA: 3 GER: 3 ITA: 3 TUR: 3 + + S1901M + + + + \ No newline at end of file diff --git a/test_render_f1901.svg b/test_render_f1901.svg new file mode 100644 index 0000000..fdefee1 --- /dev/null +++ b/test_render_f1901.svg @@ -0,0 +1,467 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + SWI + ADR + AEG + ALB + ANK + APU + ARM + BAL + BAR + BEL + BER + BLA + BOH + BRE + BUD + BUL + BUR + CLY + CON + DEN + EAS + EDI + ENG + FIN + GAL + GAS + GRE + BOT + LYO + HEL + HOL + ION + IRI + KIE + LON + LVN + LVP + MAR + MAO + MOS + MUN + NAF + NAP + NAO + NTH + NWY + NWG + PAR + PIC + PIE + POR + PRU + ROM + RUH + RUM + SER + SEV + SIL + SKA + SMY + SPA + STP + SWE + SYR + TRI + TUN + TUS + TYR + TYS + UKR + VEN + VIE + WAL + WAR + WES + YOR + + + + + RUS: 4 AUS: 3 ENG: 3 FRA: 3 GER: 3 ITA: 3 TUR: 3 + + F1901M + + + + \ No newline at end of file diff --git a/test_state.json b/test_state.json new file mode 100644 index 0000000..f624c16 --- /dev/null +++ b/test_state.json @@ -0,0 +1,71 @@ +{ + "game_state": { + "timestamp": 1770652252578290, + "zobrist_hash": "853113961011677805", + "note": "", + "name": "F1901M", + "units": { + "AUSTRIA": ["A BUD", "A VIE", "F TRI"], + "ENGLAND": ["F EDI", "F LON", "A LVP"], + "FRANCE": ["A MAR", "A PAR", "F PIC"], + "GERMANY": ["F KIE", "A BER", "A MUN"], + "ITALY": ["F NAP", "A ROM", "A VEN"], + "RUSSIA": ["A WAR", "A MOS", "F SEV", "F STP/SC"], + "TURKEY": ["F ANK", "A CON", "A SMY"] + }, + "retreats": { + "AUSTRIA": {}, + "ENGLAND": {}, + "FRANCE": {}, + "GERMANY": {}, + "ITALY": {}, + "RUSSIA": {}, + "TURKEY": {} + }, + "centers": { + "AUSTRIA": ["BUD", "TRI", "VIE"], + "ENGLAND": ["EDI", "LON", "LVP"], + "FRANCE": ["BRE", "MAR", "PAR"], + "GERMANY": ["BER", "KIE", "MUN"], + "ITALY": ["NAP", "ROM", "VEN"], + "RUSSIA": ["MOS", "SEV", "STP", "WAR"], + "TURKEY": ["ANK", "CON", "SMY"] + }, + "homes": { + "AUSTRIA": ["BUD", "TRI", "VIE"], + "ENGLAND": ["EDI", "LON", "LVP"], + "FRANCE": ["BRE", "MAR", "PAR"], + "GERMANY": ["BER", "KIE", "MUN"], + "ITALY": ["NAP", "ROM", "VEN"], + "RUSSIA": ["MOS", "SEV", "STP", "WAR"], + "TURKEY": ["ANK", "CON", "SMY"] + }, + "influence": { + "AUSTRIA": ["BUD", "VIE", "TRI"], + "ENGLAND": ["EDI", "LON", "LVP"], + "FRANCE": ["BRE", "MAR", "PAR", "PIC"], + "GERMANY": ["KIE", "BER", "MUN"], + "ITALY": ["NAP", "ROM", "VEN"], + "RUSSIA": ["WAR", "MOS", "SEV", "STP"], + "TURKEY": ["ANK", "CON", "SMY"] + }, + "civil_disorder": { + "AUSTRIA": 0, + "ENGLAND": 0, + "FRANCE": 0, + "GERMANY": 0, + "ITALY": 0, + "RUSSIA": 0, + "TURKEY": 0 + }, + "builds": { + "AUSTRIA": {"count": 0, "homes": []}, + "ENGLAND": {"count": 0, "homes": []}, + "FRANCE": {"count": 0, "homes": []}, + "GERMANY": {"count": 0, "homes": []}, + "ITALY": {"count": 0, "homes": []}, + "RUSSIA": {"count": 0, "homes": []}, + "TURKEY": {"count": 0, "homes": []} + } + } +} diff --git a/test_w1901a.json b/test_w1901a.json new file mode 100644 index 0000000..7807738 --- /dev/null +++ b/test_w1901a.json @@ -0,0 +1,71 @@ +{ + "game_state": { + "timestamp": 1770655116269363, + "zobrist_hash": "7244534327104824969", + "note": "", + "name": "W1901A", + "units": { + "AUSTRIA": ["A RUM", "A BUD", "F ADR"], + "ENGLAND": ["F LON", "A LVP", "F NTH"], + "FRANCE": ["F BRE", "A MAR", "A PAR"], + "GERMANY": ["F KIE", "A BER", "A MUN"], + "ITALY": ["F NAP", "A ROM", "A VEN"], + "RUSSIA": ["A WAR", "A MOS", "F SEV", "F STP/SC"], + "TURKEY": ["F ANK", "A CON", "A SMY"] + }, + "retreats": { + "AUSTRIA": {}, + "ENGLAND": {}, + "FRANCE": {}, + "GERMANY": {}, + "ITALY": {}, + "RUSSIA": {}, + "TURKEY": {} + }, + "centers": { + "AUSTRIA": ["BUD", "TRI", "VIE", "RUM"], + "ENGLAND": ["EDI", "LON", "LVP"], + "FRANCE": ["BRE", "MAR", "PAR"], + "GERMANY": ["BER", "KIE", "MUN"], + "ITALY": ["NAP", "ROM", "VEN"], + "RUSSIA": ["MOS", "SEV", "STP", "WAR"], + "TURKEY": ["ANK", "CON", "SMY"] + }, + "homes": { + "AUSTRIA": ["BUD", "TRI", "VIE"], + "ENGLAND": ["EDI", "LON", "LVP"], + "FRANCE": ["BRE", "MAR", "PAR"], + "GERMANY": ["BER", "KIE", "MUN"], + "ITALY": ["NAP", "ROM", "VEN"], + "RUSSIA": ["MOS", "SEV", "STP", "WAR"], + "TURKEY": ["ANK", "CON", "SMY"] + }, + "influence": { + "AUSTRIA": ["VIE", "TRI", "RUM", "BUD", "ADR"], + "ENGLAND": ["EDI", "LON", "LVP", "NTH"], + "FRANCE": ["BRE", "MAR", "PAR"], + "GERMANY": ["KIE", "BER", "MUN"], + "ITALY": ["NAP", "ROM", "VEN"], + "RUSSIA": ["WAR", "MOS", "SEV", "STP"], + "TURKEY": ["ANK", "CON", "SMY"] + }, + "civil_disorder": { + "AUSTRIA": 0, + "ENGLAND": 0, + "FRANCE": 0, + "GERMANY": 0, + "ITALY": 0, + "RUSSIA": 0, + "TURKEY": 0 + }, + "builds": { + "AUSTRIA": {"count": 1, "homes": ["TRI", "VIE"]}, + "ENGLAND": {"count": 0, "homes": []}, + "FRANCE": {"count": 0, "homes": []}, + "GERMANY": {"count": 0, "homes": []}, + "ITALY": {"count": 0, "homes": []}, + "RUSSIA": {"count": 0, "homes": []}, + "TURKEY": {"count": 0, "homes": []} + } + } +} diff --git a/verify_possible_orders.py b/verify_possible_orders.py new file mode 100644 index 0000000..064128d --- /dev/null +++ b/verify_possible_orders.py @@ -0,0 +1,80 @@ +import requests +import json +from diplomacy import Game + +BASE_URL = "http://127.0.0.1:8000" + +def get_initial_state(): + resp = requests.get(f"{BASE_URL}/game/initial-state") + return resp.json() + +def test_possible_orders_default(): + print("--- Testing Default ---") + state = get_initial_state() + resp = requests.post(f"{BASE_URL}/calculate/possible-orders", json=state) + data = resp.json() + # Expect loc -> list + if "possible_orders" in data and isinstance(data["possible_orders"], dict): + first_key = list(data["possible_orders"].keys())[0] + if isinstance(data["possible_orders"][first_key], list): + print("OK: Default format is loc -> list") + else: + print(f"FAILED: Expected list for {first_key}, got {type(data['possible_orders'][first_key])}") + else: + print("FAILED: Response structure incorrect") + +def test_possible_orders_power(): + print("--- Testing Power Name (FRANCE) ---") + state = get_initial_state() + resp = requests.post(f"{BASE_URL}/calculate/possible-orders?power_name=FRANCE", json=state) + data = resp.json() + if data.get("power") == "FRANCE" and "possible_orders" in data: + # Check if only France units are there + locs = list(data["possible_orders"].keys()) + # Initial France units are at BRE, MAR, PAR + expected = {"BRE", "MAR", "PAR"} + if set(locs) == expected: + print("OK: Filtered by power correctly") + else: + print(f"FAILED: Expected locs {expected}, got {locs}") + else: + print(f"FAILED: Response structure incorrect: {data}") + +def test_possible_orders_by_power(): + print("--- Testing By Power ---") + state = get_initial_state() + resp = requests.post(f"{BASE_URL}/calculate/possible-orders?by_power=true", json=state) + data = resp.json() + if "possible_orders" in data and "FRANCE" in data["possible_orders"]: + france_orders = data["possible_orders"]["FRANCE"] + if isinstance(france_orders, dict) and "PAR" in france_orders: + print("OK: Grouped by power correctly") + else: + print(f"FAILED: France orders structure incorrect: {france_orders}") + else: + print(f"FAILED: Response structure incorrect or FRANCE missing: {data.keys()}") + +def test_possible_orders_both(): + print("--- Testing Both (By Power + FRANCE) ---") + state = get_initial_state() + resp = requests.post(f"{BASE_URL}/calculate/possible-orders?by_power=true&power_name=FRANCE", json=state) + data = resp.json() + # My implementation returns just that power in filtered format if both provided? + # Actually, my code: + # if by_power: + # ... + # if power_name: + # return {"power": power_name, "possible_orders": results[power_name]} + if data.get("power") == "FRANCE" and "possible_orders" in data: + print("OK: Both parameters handled correctly (returned filtered power)") + else: + print(f"FAILED: Response structure incorrect: {data}") + +if __name__ == "__main__": + try: + test_possible_orders_default() + test_possible_orders_power() + test_possible_orders_by_power() + test_possible_orders_both() + except Exception as e: + print(f"Error during testing: {e}") diff --git a/verify_process.py b/verify_process.py new file mode 100644 index 0000000..23dd44f --- /dev/null +++ b/verify_process.py @@ -0,0 +1,184 @@ +import requests +import json + +BASE_URL = "http://127.0.0.1:8000" + +payload = { + "game_state": { + "controlled_powers": None, + "daide_port": None, + "deadline": 0, + "error": [], + "game_id": "aGelgNaM_tiPdor5", + "map_name": "standard", + "message_history": {}, + "messages": [], + "meta_rules": [], + "n_controls": 0, + "no_rules": [], + "note": "", + "observer_level": None, + "order_history": {}, + "outcome": [], + "phase": "SPRING 1901 MOVEMENT", + "phase_abbr": "", + "powers": { + "AUSTRIA": { + "abbrev": "A", + "adjust": [], + "centers": ["BUD", "TRI", "VIE"], + "civil_disorder": 0, + "controller": {"1770333163210986": "dummy"}, + "homes": ["BUD", "TRI", "VIE"], + "influence": ["BUD", "VIE", "TRI"], + "name": "AUSTRIA", + "order_is_set": 0, + "orders": {}, + "retreats": {}, + "role": "server_type", + "tokens": [], + "units": ["A BUD", "A VIE", "F TRI"], + "vote": "neutral", + "wait": True + }, + "ENGLAND": { + "abbrev": "E", + "adjust": [], + "centers": ["EDI", "LON", "LVP"], + "civil_disorder": 0, + "controller": {"1770333163211038": "dummy"}, + "homes": ["EDI", "LON", "LVP"], + "influence": ["EDI", "LON", "LVP"], + "name": "ENGLAND", + "order_is_set": 0, + "orders": {}, + "retreats": {}, + "role": "server_type", + "tokens": [], + "units": ["F EDI", "F LON", "A LVP"], + "vote": "neutral", + "wait": True + }, + "FRANCE": { + "abbrev": "F", + "adjust": [], + "centers": ["BRE", "MAR", "PAR"], + "civil_disorder": 0, + "controller": {"1770333163211088": "dummy"}, + "homes": ["BRE", "MAR", "PAR"], + "influence": ["BRE", "MAR", "PAR"], + "name": "FRANCE", + "order_is_set": 0, + "orders": {}, + "retreats": {}, + "role": "server_type", + "tokens": [], + "units": ["F BRE", "A MAR", "A PAR"], + "vote": "neutral", + "wait": True + }, + "GERMANY": { + "abbrev": "G", + "adjust": [], + "centers": ["BER", "KIE", "MUN"], + "civil_disorder": 0, + "controller": {"1770333163211135": "dummy"}, + "homes": ["BER", "KIE", "MUN"], + "influence": ["KIE", "BER", "MUN"], + "name": "GERMANY", + "order_is_set": 0, + "orders": {}, + "retreats": {}, + "role": "server_type", + "tokens": [], + "units": ["F KIE", "A BER", "A MUN"], + "vote": "neutral", + "wait": True + }, + "ITALY": { + "abbrev": "I", + "adjust": [], + "centers": ["NAP", "ROM", "VEN"], + "civil_disorder": 0, + "controller": {"1770333163211181": "dummy"}, + "homes": ["NAP", "ROM", "VEN"], + "influence": ["NAP", "ROM", "VEN"], + "name": "ITALY", + "order_is_set": 0, + "orders": {}, + "retreats": {}, + "role": "server_type", + "tokens": [], + "units": ["F NAP", "A ROM", "A VEN"], + "vote": "neutral", + "wait": True + }, + "RUSSIA": { + "abbrev": "R", + "adjust": [], + "centers": ["MOS", "SEV", "STP", "WAR"], + "civil_disorder": 0, + "controller": {"1770333163211226": "dummy"}, + "homes": ["MOS", "SEV", "STP", "WAR"], + "influence": ["WAR", "MOS", "SEV", "STP"], + "name": "RUSSIA", + "order_is_set": 0, + "orders": {}, + "retreats": {}, + "role": "server_type", + "tokens": [], + "units": ["A WAR", "A MOS", "F SEV", "F STP/SC"], + "vote": "neutral", + "wait": True + }, + "TURKEY": { + "abbrev": "T", + "adjust": [], + "centers": ["ANK", "CON", "SMY"], + "civil_disorder": 0, + "controller": {"1770333163211272": "dummy"}, + "homes": ["ANK", "CON", "SMY"], + "influence": ["ANK", "CON", "SMY"], + "name": "TURKEY", + "order_is_set": 0, + "orders": {}, + "retreats": {}, + "role": "server_type", + "tokens": [], + "units": ["F ANK", "A CON", "A SMY"], + "vote": "neutral", + "wait": True + } + }, + "registration_password": None, + "result_history": {}, + "role": "server_type", + "rules": ["NO_DEADLINE", "CD_DUMMIES", "ALWAYS_WAIT", "SOLITAIRE", "NO_PRESS", "IGNORE_ERRORS", "POWER_CHOICE"], + "state_history": {}, + "status": "forming", + "timestamp_created": 1770333163210884, + "victory": [18], + "win": 18, + "zobrist_hash": 1919110489198082600 + }, + "orders": { + "FRANCE": ["A MAR - PIE", "A PAR - BUR"] + } +} + +try: + resp = requests.post(f"{BASE_URL}/calculate/process", json=payload) + print(f"Status Code: {resp.status_code}") + if resp.status_code == 200: + result = resp.json() + print(f"New Phase: {result['game_state']['phase']}") + print(f"France Units: {result['game_state']['powers']['FRANCE']['units']}") + # Check order history + history = result['game_state']['order_history'] + print(f"Order History Keys: {list(history.keys())}") + if 'SPRING 1901 MOVEMENT' in history: + print(f"France Spring 1901 Orders: {history['SPRING 1901 MOVEMENT'].get('FRANCE')}") + else: + print(f"Error Response: {resp.text}") +except Exception as e: + print(f"Exception: {str(e)}")