Skip to content

davidbell81/TRC

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

1 Commit
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

TRC Game

A hex-based tactical board game with Godot frontend and FastAPI backend.

Quick Start

Backend

cd backend
pip install -e .
uvicorn app.main:app --reload

Frontend

  1. Open godot/project.godot in Godot 4.3
  2. Run the project

Project Structure

This document is our working plan and starter scaffolds now that we will focus on Godot for the frontend and use minimal Cursor help for backend utilities.

Repo layout

trc/ README.md backend/ app/ init.py main.py # FastAPI factory + CORS api.py # Routers for /legal_moves, /apply_move, /resolve_combat, /ai_move, /save, /load models.py # Pydantic models (Hex, Unit, GameState, Move, CombatResolution, etc.) engine/ init.py rules.py # Pure functions for rules; deterministic map.py # Hex/zone of control logic, terrain rng.py # Deterministic RNG wrapper storage/ init.py memory.py # In-memory Store for dev tests/ test_rules.py test_api.py pyproject.toml uv.lock # or poetry.lock / requirements.txt (choose one) godot/ project.godot scenes/ Main.tscn Board.tscn scripts/ GameClient.gd # HTTP client, state sync HexGrid.gd # Axial grid math + drawing + picking Board.gd # Renders units, handles clicks, highlights legal moves assets/ images/ # temporary placeholders; VASSAL-sourced images later (private dev use) exports/ web/ # HTML5 export preset targeting iPad Safari docs/ api_contract.md rules_notes.md

API Contract

See docs/api_contract.md for the complete API specification.

Quick Reference

All endpoints return { "ok": true, "data": ... } on success and { "ok": false, "error": "..." } on failure.

Models // Hex { "q": 0, "r": 0 }

// Move { "from": {"q":0,"r":0}, "to": {"q":1,"r":0}, "unit_id": "G1" }

// GameState (minimal) { "id":"game_001", "turn":1, "side":"Axis", "units":[{"id":"G1","side":"Axis","q":0,"r":0}], "seed": 12345 } Endpoints

GET /health → { ok:true, data:{version:"0.1"} }

POST /new_game body: { "seed": 12345 } → { ok:true, data:GameState }

POST /legal_moves body: { "state": GameState, "unit_id": "G1" } → { ok:true, data:[Hex...] }

POST /apply_move body: { "state": GameState, "move": Move } → { ok:true, data:GameState }

POST /resolve_combat body: { "state": GameState, "combats": [...] } → { ok:true, data:GameState }

POST /ai_move body: { "state": GameState } → { ok:true, data:{ move: Move, state: GameState } }

POST /save body: { "state": GameState } → { ok:true }

POST /load body: { "id": "game_001" } → { ok:true, data: GameState }

Backend

See backend/README.md for setup instructions.

FastAPI Implementation

backend/app/main.py

from fastapi import FastAPI from fastapi.middleware.cors import CORSMiddleware from .api import router

def create_app() -> FastAPI: app = FastAPI(title="TRC API", version="0.1") app.add_middleware( CORSMiddleware, allow_origins=[""], allow_credentials=True, allow_methods=[""], allow_headers=["*"], ) app.include_router(router) return app

app = create_app()

backend/app/api.py

from fastapi import APIRouter from pydantic import BaseModel from typing import List, Optional, Literal

router = APIRouter()

class Hex(BaseModel): q: int r: int

class Unit(BaseModel): id: str side: Literal["Axis","Soviet"] q: int r: int

class GameState(BaseModel): id: str turn: int side: Literal["Axis","Soviet"] units: List[Unit] seed: int

class Move(BaseModel): unit_id: str from_: Hex to: Hex

@router.get("/health") def health(): return {"ok": True, "data": {"version": "0.1"}}

@router.post("/new_game") def new_game(seed: Optional[int] = 12345): state = { "id": "game_001", "turn": 1, "side": "Axis", "units": [{"id":"G1","side":"Axis","q":0,"r":0}], "seed": seed or 12345, } return {"ok": True, "data": state}

@router.post("/legal_moves") def legal_moves(payload: dict): # TODO: call engine.rules.legal_moves(...) return {"ok": True, "data": [{"q":1,"r":0},{"q":0,"r":1}]}

@router.post("/apply_move") def apply_move(payload: dict): # TODO: apply and return updated state return {"ok": True, "data": payload.get("state")}

backend/app/engine/rng.py

from dataclasses import dataclass import random

@dataclass class DeterministicRNG: seed: int def post_init(self): self._rng = random.Random(self.seed) def roll(self, sides: int = 6) -> int: return self._rng.randint(1, sides) def choice(self, seq): return self._rng.choice(seq)

backend/app/engine/rules.py

from .rng import DeterministicRNG

def legal_moves(state, unit_id): # Placeholder: return axial neighbors within map bounds return []

backend/pyproject.toml (uv or poetry compatible)

[project] name = "trc-backend" version = "0.1.0" dependencies = [ "fastapi", "uvicorn", "pydantic>=2", ] [tool.pytest.ini_options] pythonpath = ["app"]

Frontend

See godot/README.md for setup instructions.

Godot Implementation

Scene tree

Main.tscn → root Node with a Board child and an HTTPRequest node

Board.tscn → Node2D with script Board.gd and a HexGrid script attached or instantiated

Axial math and drawing

godot/scripts/HexGrid.gd

extends Node2D

class_name HexGrid

const HEX_SIZE := 40.0

func axial_to_pixel(q: int, r: int) -> Vector2: var x = HEX_SIZE * (3.0/2.0 * q) var y = HEX_SIZE * (sqrt(3)/2.0 * q + sqrt(3) * r) return Vector2(x, y)

func hex_corners(center: Vector2) -> PackedVector2Array: var pts: PackedVector2Array = [] for i in 6: var angle = deg_to_rad(60 * i + 30) pts.append(center + Vector2(HEX_SIZE * cos(angle), HEX_SIZE * sin(angle))) return pts

func _draw(): # simple 10x10 grid for q in range(-5,5): for r in range(-5,5): var c = axial_to_pixel(q,r) draw_polyline(hex_corners(c), Color.WHITE, 2) Board input and highlighting

godot/scripts/Board.gd

extends Node2D

@onready var grid: HexGrid = $HexGrid var legal: Array[Vector2i] = []

func set_legal_moves(axial_list): legal.clear() for h in axial_list: legal.append(Vector2i(h.q, h.r)) update()

func _draw(): grid._draw() for h in legal: var c = grid.axial_to_pixel(h.x, h.y) draw_circle(c, 8, Color(0,1,0,0.6)) HTTP client glue

godot/scripts/GameClient.gd

extends Node

@onready var http: HTTPRequest = $HTTPRequest @onready var board = $Board var api_base := "http://127.0.0.1:8000"

func _ready(): new_game()

func new_game(): var url = api_base + "/new_game" http.request(url, [], HTTPClient.METHOD_POST)

func _on_HTTPRequest_request_completed(result, response_code, headers, body): var json = JSON.parse_string(body.get_string_from_utf8()) if json and json.ok: # Example: request legal moves for unit G1 var state = json.data var payload = JSON.stringify({"state": state, "unit_id": "G1"}) http.request(api_base+"/legal_moves", [], HTTPClient.METHOD_POST, payload) elif json and json.data and json.data is Array: board.set_legal_moves(json.data)

Wire the HTTPRequest node signal request_completed to _on_HTTPRequest_request_completed.

Web Export

For iPad Safari deployment:

Godot 4.3 Web export preset

Canvas/WebGL: WebGL2

Threads: Off for iOS Safari

GDNative: not needed

Limit initial memory to a sensible size; test loading on iPad

Serve with proper Cross-Origin-Opener-Policy/Cross-Origin-Embedder-Policy disabled for single-thread build

Game Rules

See docs/rules_notes.md for detailed rules and notes.

Determinism Strategy

Backend is source of truth for state transitions and RNG via DeterministicRNG(seed)

Include seed and a history array in state for debugging

All combat results come from backend; client only renders

Testing

Test suite covers rules and API endpoints:

test_rules.py: unit adjacency, ZOC basics, movement allowance

test_api.py: schema contracts and idempotent apply_move

Golden-state roundtrip: /new_game → /apply_move → /save → /load

About

digital verstion of TRC

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors