Search

Saturday, August 1, 2026

Build a Cross-Platform Ludo Game with ChatDev 2.0: Free AI Models Fail, a $1 DeepSeek Run Ships It

Runbook · Multi-agent application build · Part 3

SANYALnet Labs Ludo AI Arena running on Linux: a rendered cross-shaped Ludo board with four AI players, a die, and an event log showing model-generated move reasoning.
The finished game: four autonomous AI players, a live event log of model-chosen moves, and a die that flashes without shoving the board around.

Building a Desktop Game with ChatDev 2.0: When Free AI Models Fail, and a One-Dollar DeepSeek Run Ships It

In Part 2 of this series, ChatDev 2.0 took a detailed specification and built a Live News Debate Wall—a self-contained Python web service that turned business headlines into a fictional AI-parody debate. That project answered a friendly question: can the virtual software company build a real application from a real spec? It could. This time the question was less friendly.

I wanted two things the previous run never tested. First, a product in a language the workflow does not default to: C# on .NET 10 with Avalonia—a compiled, cross-platform desktop GUI, not a Python script. Second, I wanted to stop treating the model as a fixed part of the furniture. Part 1 and Part 2 ran on GLM. Here I went shopping—starting with the most powerful free model I could find and working down, then sideways, then finally reaching for a paid endpoint. The interesting story turned out not to be what got built, but which models could build it at all.

The goal

The assignment was a desktop board game: SANYALnet Labs Ludo AI Arena. Four autonomous AI players (Red, Green, Yellow, Blue), each with four tokens and its own separate NVIDIA NIM session, playing a full game of Ludo automatically—animated die, tokens gliding cell by cell, captures, blockades, bonus rolls, a winner screen. Crucially, each AI player asks a language model to pick its move from an engine-generated list of legal moves, and the whole thing must fall back to a local deterministic AI the instant the network misbehaves.

Division of labor

Two different model jobs, deliberately kept separate: a builder model that writes the C#/Avalonia application through ChatDev's agent graph, and a small runtime model that each token consults during play. The builder needs coding muscle and reliable tool-calling for hours. The move-picker just reads a compact board state and returns one moveId. Asking one model to be architect, programmer, reviewer, test engineer and tiny plastic token strategist is how you get a very colorful loading screen.

The scoreboard, up front

Here is the whole experiment in one table before the play-by-play. Five free NVIDIA NIM models attempted the build. Each failed differently. One paid model finished.

Builder model Tier Result
Nemotron 3 Ultra 550B free Rate-limited to death—starved before it could fix anything.
Nemotron super 49B free Rock-solid, but under-built: a stub UI, no real board.
Nemotron super 120B free Capable and reliable—then silently hung on a model call. Twice.
poolside Laguna XS 2.1 free Wrote real code, then broke its own build tooling and could not debug it—then hung.
DeepSeek V4 Pro paid (~$1) Ran the full lifecycle to completion—a launching, playable game.
What worked, in one line

Only the paid frontier model completed the build—and the successful run cost about one dollar, not a subscription. The rest of this article is why the free tier kept losing, and what "done" actually looked like once a capable model finished.

In a hurry? Skip the experiment

The next several sections are the honest, educational account of the free models that failed—interesting, but not needed to get a working game. If you just want to build and play it, jump straight to Build it yourself, end to end and follow the numbered steps.

The requirements prompt

As in Part 2, the detailed specification lived in a Markdown file attached to the run. It fixes the board geometry down to individual cell coordinates, the rule profile, the turn state machine, the NIM request and response contract, the retry and fallback behaviour, and the exact acceptance criteria. Scroll it if you enjoy that sort of thing:

CHATDEV TASK PROMPT — PROJECT: SANYALnet Labs Ludo AI Arena
REVISION: 3   SCOPE: Four autonomous AI players only

Mandatory requirements distilled from a working reference build (all enforced by the
acceptance list, section 16):
(a) all user-visible header/title text reads exactly "SANYALnet Labs Ludo AI Arena";
(b) a fixed bottom-right copyright notice "(C) Supratim Sanyal + AI Agents" on every screen;
(c) tokens GLIDE smoothly between cells (interpolated, ~300-350 ms/cell, relaxed) for EVERY
    color and EVERY move type including single-cell entry moves — never teleport;
(d) the in-flight piece carries a gentle pulsing highlight so it is easy to follow;
(e) the die emphasis FLASHES (color pulse) at a fixed size; the board never changes size or
    position during play;
(f) NVIDIA NIM integration is correct: the request URL preserves the "/v1" path and the
    response is read from choices[0].message.content; a valid key+model yields REAL model
    moves, never a silent 100% fallback;
(g) the game runtime targets the free NIM tier with a small model, deliberate request spacing,
    a short retry budget, and an authoritative local fallback (section 8, 10).

1. MISSION AND COMPLETION STANDARD
Build a polished, cross-platform desktop Ludo game. Solution/assembly identifiers use
"LudoNimArena"; all USER-VISIBLE header/title text reads exactly "SANYALnet Labs Ludo AI
Arena". C#, .NET 10, Avalonia UI; runs on Linux desktop. Exactly four autonomous AI
players (Red, Green, Yellow, Blue), four tokens each, each with a separate NVIDIA NIM
session. The game auto-plays with visible animation until one player brings all four
tokens home.

Turn order of visible events: (1) highlight the acting player; (2) generate the die value
locally; (3) animate the roll without exposing the stored value to the AI; (4) end the
animation on the stored value and reveal it; (5) generate legal moves locally; (6) if any
legal move exists, call that player's AI session; (7) validate the returned move against
the legal list; (8) animate then apply it; (9) resolve captures/blockades/home/finish/
bonus/turn-end; (10) repeat for the same player on a bonus, else advance clockwise. The AI
must never choose, alter, reroll, predict, or see the die value before the roll animation
completes; the animation never alters the value.

Do not stop at planning, scaffolding, or compilation. Implement, review, build, test,
render, inspect, correct, and document the whole app. Never claim success for a command or
visual check not actually run. Continue until two successive full review passes find no new
defect.

2. HARD SCOPE
Include: exactly four autonomous AI players; four tokens each; automatic play from roll-off
to victory; one animated die roll before every normal, bonus, and roll-off roll; a separate
NIM decision context per player; a deterministic local fallback AI when NIM is unavailable;
a 15x15 Ludo board drawn with Avalonia vector APIs; animated die and smooth cell-by-cell
token movement; an always-visible "QUIT" button in-game; a winner screen with "NEW GAME"
and "QUIT"; cancellation-aware backoff for overload/retry (incl. HTTP 529 and server waits
>10 min).

Exclude in R1: human players, player-count or Human/AI selectors, clickable token controls,
remote play, accounts, matchmaking, browser UI, web server, database. Keep the engine
independent of the player controller: define an extension point (e.g. IPlayerController) so
a future HumanPlayerController can be added without rewriting rules; a reserved enum/stub is
fine. Ship no working human controller or human-play controls.

3. WORKFLOW COMPATIBILITY AND WORKSPACE SAFETY
Use the general ChatDev_v1 workflow. Its stale generic text ("Programming Language: Python"
and Python execution helpers) is OVERRIDDEN: the product is C#, target net10.0, UI Avalonia.
Reject/replace any Python product implementation. Do not modify the ChatDev source tree,
workflow YAML, prior WareHouse projects, shell startup files, .bashrc, or system config —
work only in this run's code_workspace. A temporary Python helper under scripts/, run via
uv, may be used SOLELY to invoke dotnet and capture stdout/stderr/exit codes (build infra,
not product code); the app must not need Python/uv at runtime. Never copy or print SSH
credentials. Never write NVIDIA_API_KEY into source, config, logs, tests, docs, screenshots,
or artifacts — read it only from the inherited environment.

At start, record the effective environment: id; pwd; uname -a; dotnet --info;
dotnet --list-sdks; DISPLAY; WAYLAND_DISPLAY. Require a .NET 10 SDK and target net10.0 (no
silent downgrade); pin it in global.json with a patch roll-forward policy. Use stable
Avalonia packages compatible with .NET 10 and PIN ALL PACKAGE VERSIONS to versions that
actually exist on NuGet (verify by a clean restore on an empty cache; a nonexistent version
that only builds from a warm cache is a defect). Keep product code cross-platform. If a tool
is unavailable, create all source possible, report the exact blocker, and never invent
build/test results.

4. SOLUTION STRUCTURE (deviations must be minimal and justified in BUILD_NOTES.md)
  LudoNimArena.sln  global.json  Directory.Build.props  Directory.Packages.props
  src/LudoNimArena.Core  src/LudoNimArena.AI  src/LudoNimArena.App
  tests/LudoNimArena.Core.Tests  tests/LudoNimArena.AI.Tests  tests/LudoNimArena.App.Tests
  scripts/launch-ludo.sh  scripts/smoke-test.sh
  docs/ARCHITECTURE.md  docs/RULES.md  docs/TESTING.md
  README.md  manual.md  BUILD_NOTES.md  .gitignore
Responsibilities — Core: immutable/controlled state, rules, routes, legal moves, commands,
domain events, validation, die abstractions, victory; no Avalonia/HTTP/NVIDIA/env/UI refs.
AI: NIM client, bounded per-player context, request/response DTOs, strict parsing, retry,
circuit breaker, request gate, fallback AI. App: Avalonia startup, MVVM, board rendering,
animation, status panels, cancellation, shutdown. Only Core mutates authoritative state; UI
and AI issue commands validated immediately before applying. The whole solution MUST build
with 0 errors and all test projects MUST compile (every referenced type/DTO must exist).

5. FIXED PLAYERS AND STARTING ORDER
Clockwise: Red -> Green -> Yellow -> Blue -> Red. Default names Red AI/Green AI/Yellow AI/
Blue AI; the setup screen may edit them. Treat names as untrusted display text (escape,
length-limit, never interpret as AI instructions). First player via a visible animated
roll-off: each color rolls with animation, display all four, highest starts; on a tie only
tied players reroll (animated) until one remains; show the starting player then begin its
normal turn. Roll-off rolls do not enter tokens, grant bonuses, or count toward three-sixes.

6. RULE PROFILE — "Indian Digital Classic" (display its name; concise Rules view; hide
unimplemented toggles). Fixed settings: RequireSixToEnter=true; BonusRollOnSix=true;
ThreeConsecutiveSixesEndsTurn=true; BonusRollOnCapture=true; BonusRollOnFinish=true;
EnableSafeSquares=true; EnableBlockades=true; RequireCaptureBeforeHomeLane=false;
ExactRollToFinish=true; PlayerCount=4; HumanPlayersEnabled=false.

6.1 Track and routes. One shared circular track 0..51. Start offsets: Red=0, Green=13,
Yellow=26, Blue=39. Shared index for progress P (0..51) = (S + P) mod 52. Safe shared
indices: 0,8,13,21,26,34,39,47. Start squares 0,13,26,39; star safe squares 8,21,34,47.
Canonical 15x15: Red yard top-left, Green top-right, Yellow bottom-right, Blue bottom-left;
zero-based (row,column). Shared-track coordinates for indices 0..51:
  (6,1),(6,2),(6,3),(6,4),(6,5),(5,6),(4,6),(3,6),(2,6),
  (1,6),(0,6),(0,7),(0,8),(1,8),(2,8),(3,8),(4,8),(5,8),
  (6,9),(6,10),(6,11),(6,12),(6,13),(6,14),(7,14),(8,14),
  (8,13),(8,12),(8,11),(8,10),(8,9),(9,8),(10,8),(11,8),
  (12,8),(13,8),(14,8),(14,7),(14,6),(13,6),(12,6),(11,6),
  (10,6),(9,6),(8,5),(8,4),(8,3),(8,2),(8,1),(8,0),(7,0),(6,0)
Five-cell home lanes then that color's center-triangle cell:
  Red: (7,1),(7,2),(7,3),(7,4),(7,5)   Green: (1,7),(2,7),(3,7),(4,7),(5,7)
  Yellow: (7,13),(7,12),(7,11),(7,10),(7,9)   Blue: (13,7),(12,7),(11,7),(10,7),(9,7)
Keep this table in ONE authoritative geometry component shared by rendering and tests; do
not duplicate or re-infer coordinates. Each route: shared progress 0..51 (52 cells, starting
on the color's start square); progress 52..56 (five home-lane cells); progress 57 (center
home). A token needs exactly 57 steps from start to center home; other colors can never
enter its home lane. Add route/rotation tests for all colors. Token state is exactly one of
InYard, OnSharedTrack, InHomeLane, Finished.

6.2 Movement. All tokens start in yard. A six is required to move a yard token to progress 0
(entering places it on the start square without an extra six-step move). On a six every legal
choice is allowed (enter or move six). A token moves exactly the rolled number, cannot
overshoot progress 57; a finished token never moves; if no token can legally use a roll, none
moves.

6.3 Captures/safe. Landing on a NON-safe shared square holding exactly one opponent token
captures it (return to yard). No capture on a safe square, home lane, or center. Different
colors may share a safe square; a same-color pair on a safe square is not a blockade; the
renderer offsets tokens sharing a square so each is visible. A capture grants one bonus roll
unless the move wins the game.

6.4 Blockades (non-safe shared square). Two same-color tokens form a blockade an opponent
cannot land on or pass through; the owner may move either token out and may pass through its
own blockade; no third same-color token may land there; a lone opponent cannot capture a
blockade. Legal-move generation MUST inspect every traversed square, not just the
destination. Mixed-color occupancy on a non-safe square is never stable (a legal landing
captures the lone opponent). Reject states violating these rules.

6.5 Sixes/bonus. A six grants one bonus roll after its move (or after a no-move), unless it
is the third consecutive six. Count consecutive sixes only within the same continuing turn:
increment on a six; reset to zero after any non-six (even if the player continues via capture/
finish); reset at turn end. On the third consecutive six: reveal the animated value, forfeit
the roll, generate no legal moves, make no NIM request, move nothing, show a clear message,
reset the count, end the turn. Bonuses never stack (six+capture = one extra roll). A winning
move ends the game immediately with no further roll.

6.6 Home/victory. After shared progress 51, movement enters only the token's own home lane;
home-lane and finished tokens are safe; exact movement is required to reach progress 57; no
prior capture is required to enter the home lane. First player with all four tokens at
progress 57 wins; stop turn processing immediately (no later places).

7. TURN STATE MACHINE AND DIE ANIMATION
Explicit, cancellation-aware state machine, e.g.: Setup, DeterminingFirstPlayer,
PreparingTurn, GeneratingDieResult, AnimatingDie, RevealingDieResult, GeneratingLegalMoves,
WaitingForAiDecision, ValidatingAiDecision, AnimatingTokenMove, ResolvingMove,
AnimatingCapture, PreparingBonusRoll, AdvancingTurn, GameOver, ShuttingDown. Use unique
GameId/TurnId/RollId/PlayerId/RequestId; no stale callback or AI response may mutate a
different game/turn/roll/player.

Per normal or bonus roll: (1) highlight the actor; (2) generate one authoritative value via
an injected IDieRoller; (3) store it, do not expose to the AI; (4) animate ~700-1100 ms in
normal mode; (5) show cosmetic changing faces from a SEPARATE animation source (do not
consume authoritative RNG to animate); (6) end on the stored value; (7) mark complete and
reveal; (8) apply the third-six rule; (9) generate the full legal-move list; (10) if empty,
make no NIM call and resolve the six/advance; (11) else (including one forced move) call the
player's AI session with the revealed value; (12) validate the returned MoveId, animate the
token cell-by-cell, resolve. Status progresses through messages like "Green AI is rolling…",
"Green AI rolled 5", "Green AI is choosing a move…". A bonus roll repeats the FULL animation;
never reuse a value or skip animation.

Use separate injectable abstractions for authoritative RNG, cosmetic faces, time, and
animation completion. Production rolls unbiased (RandomNumberGenerator.GetInt32(1,7) behind
IDieRoller); tests use seeded/scripted values and fake time. Never block the UI thread; avoid
async void except framework handlers. Record domain events sufficient to reconstruct/test the
sequence: GameStarted, StartingRollCompleted, StartingPlayerSelected, TurnStarted,
DieResultGenerated, DieAnimationStarted, DieAnimationCompleted, DieResultRevealed,
LegalMovesGenerated, AiDecisionRequested, AiDecisionReceived, AiDecisionRejected,
FallbackDecisionSelected, TokenEntered, TokenMoved, TokenCaptured, BlockadeFormed,
BlockadeBroken, TokenFinished, BonusRollAwarded, ThirdSixForfeited, TurnEnded, PlayerWon,
GameCancelled.

8. FOUR NVIDIA NIM AI PLAYERS
One bounded AiPlayerSession per color. Sessions may share the configured model but MUST NOT
share persona, history buffers, request/failure counters, or mutable decision state. Strategy
hints (preferences only, never alter rules/legal moves): Red assertive-but-legal, Green
safety-conscious, Yellow progress-focused, Blue balanced. Keep each session's history short/
bounded; do not rely on server-side conversation state.

8.1 Configuration — OpenAI-compatible NVIDIA NIM chat-completions API.
  Base URL default: https://integrate.api.nvidia.com/v1   Path: /chat/completions
  Auth: Authorization: Bearer <NVIDIA_API_KEY>   Content-Type/Accept: application/json
  Mode: stream=false
Typed settings (env): NVIDIA_API_KEY, NVIDIA_MODEL, NVIDIA_SECONDARY_MODEL, NVIDIA_BASE_URL,
NVIDIA_REQUEST_TIMEOUT_SECONDS, NVIDIA_MAX_RETRY_DELAY_SECONDS,
NVIDIA_MAX_RETRY_ELAPSED_SECONDS, NVIDIA_MIN_CALL_INTERVAL_SECONDS,
NVIDIA_CIRCUIT_BREAKER_SECONDS, NVIDIA_FAILURE_POLICY. Defaults are tuned for a live game on
the FREE NIM tier: a small non-reasoning model, request spacing under the ~40 RPM ceiling, a
short retry budget, then local fallback:
  NVIDIA_MODEL=nvidia/nemotron-mini-4b-instruct   # small, non-reasoning, function-calling tuned
  NVIDIA_SECONDARY_MODEL=                          # optional hosted failover; EMPTY = skip it
  NVIDIA_BASE_URL=https://integrate.api.nvidia.com/v1
  NVIDIA_REQUEST_TIMEOUT_SECONDS=10  NVIDIA_MIN_CALL_INTERVAL_SECONDS=3   # <= ~20 calls/min
  NVIDIA_MAX_RETRY_DELAY_SECONDS=5   NVIDIA_MAX_RETRY_ELAPSED_SECONDS=15  # then local fallback
  NVIDIA_CIRCUIT_BREAKER_SECONDS=30  NVIDIA_FAILURE_POLICY=wait-then-fallback
SECONDARY / FAILOVER: support an optional NVIDIA_SECONDARY_MODEL. On a primary-model failure
(timeout, 429, 529, or malformed result) within the retry budget, if NVIDIA_SECONDARY_MODEL is
NON-EMPTY, try it once at the same endpoint; if it is EMPTY, SKIP hosted failover and go
straight to the local deterministic AI. Keep the secondary code path present but inert when the
name is empty. Do NOT reuse ChatDev's build-time retry values (dozens of attempts, 20-minute
delays, multi-hour windows) at game time — during a match use at most 2-3 attempts and a ~15 s
total budget, then fall back locally. The local deterministic AI is authoritative whenever NIM
is unavailable; the game must never stall on the network. All values are env-overridable.
NVIDIA_API_KEY is required for live decisions; NVIDIA_MODEL overrides the default. Keep the
default in non-secret typed config (changeable without code edits). If the key is missing or
the model is rejected/unavailable, start normally, warn clearly, and use fallback. Never log/
display the key, bearer header, full environment, or secrets; redact them in exceptions.

8.2 URL AND RESPONSE PARSING — CORRECTNESS (previous builds shipped two silent, show-stopping
defects here; both are forbidden and MUST have regression tests):
  (a) URL: the effective request URL MUST be the configured base URL WITH its full path
      preserved, plus "/chat/completions" — i.e. https://integrate.api.nvidia.com/v1/chat/
      completions. Do NOT drop the "/v1" segment. With System.Net.Http this happens when
      HttpClient.BaseAddress lacks a trailing slash and the request path starts with "/"
      (a leading-slash path is resolved from the host root, discarding "/v1"). Build the URL
      safely: post to an absolute URL string composed as baseUrl.TrimEnd('/') +
      "/chat/completions" (or set BaseAddress ending in "/v1/" and use a relative path with NO
      leading slash). A request that 404s because "/v1" was dropped is a defect.
  (b) RESPONSE: the HTTP body is an OpenAI-style envelope
      {"choices":[{"message":{"content":"<the move JSON as a string>"}}]}. Parse
      choices[0].message.content FIRST, then parse the strict move JSON from that content
      string. Do NOT deserialize the raw envelope into the move DTO (it has no top-level
      moveId, so the move would always be empty and the game would silently fall back).
  (c) NO-SILENT-FALLBACK: when a valid key and model are present the app MUST actually use
      model decisions. Log, per turn, whether the move came from NIM or fallback, and expose a
      visible per-player "last decision source" indicator. Acceptance requires demonstrating
      real NIM-driven moves (not 100% fallback) in a live check.

8.3 Request timing/content. One NIM request per AI roll that has >=1 legal move (even a single
forced move). No request when: game over, no legal move, third-six forfeit, cancellation
requested, or stale request identity. Send compact JSON: GameId/TurnId/RollId/RequestId/
PlayerId/color; strategy hint; revealed die value and consecutive-six count; concise token
positions for all players; safe-square and blockade info; recent public events; the complete
engine-generated legal-move list; a strict response contract. Never send secrets, drawing
coordinates, full logs, hidden chain-of-thought, or another player's private history.
Example legal move:
  {"moveId":"red-token-2:18->24","tokenId":"red-token-2","from":"track:18","to":"track:24",
   "entersBoard":false,"captures":["green-token-1"],"landsSafe":false,"finishes":false,
   "formsBlockade":false}
Required response (exactly one compact object):
  {"moveId":"red-token-2:18->24","reason":"Captures an exposed opponent while advancing."}
Use stream=false, temperature 0, top_p 1, and a small output limit (max_tokens ~64) in greedy /
non-reasoning mode — the recommended setting for these small models. The reply is a tiny JSON
object, not an essay. (If a reasoning-capable model is configured instead, raise max_tokens
enough that it finishes reasoning AND still emits the JSON, e.g. 512-1024, or it returns empty
content and forces fallback.) Keep the request compact (< ~1500 tokens): color, die, consecutive-
six count, token positions, safe squares, blockades, the legal-move list, and a short strategy
hint — nothing else. Do NOT expose callable tools/functions to the model; require plain JSON
content parsed with System.Text.Json (strip harmless Markdown fences). Require the exact moveId
(+ optional short reason); reject prose-only output, multiple objects, unknown fields, unknown
MoveIds, altered state, commands, or code. One primary request and at most one repair request
per eligible roll
(repair carries only the validation error, allowed MoveIds, and exact shape); if repair fails,
use fallback. Treat reason as untrusted display text (escape, normalize control chars, cap 160
chars). Before applying a response verify GameId/TurnId/RollId/PlayerId/RequestId; discard
stale responses without mutating state.

9. LOCAL FALLBACK AI
A complete deterministic local AI that can finish a game without NIM, choosing only from
engine-generated legal moves. Documented scoring/priority considering: immediate victory;
finishing a token; capturing; escaping capture risk; landing safe; entering a yard token;
forming/preserving a useful blockade; breaking a harmful one; forward progress/distance to
home; nearby opponents/avoidable exposure; keeping >1 useful active token. Deterministic
tie-break (token ID then MoveId; no randomness). Label every fallback decision "Local fallback
AI"; never present it as an NVIDIA decision.

10. RETRY, THROTTLING, CIRCUIT BREAKER
One cancellation-aware async request gate so only one live NIM request/retry runs at a time
(prevent a four-player stampede); apply the minimum interval; release the gate on every
success/exception/timeout/cancel. Retry transient failures: HTTP 408,425,429,500,502,503,504,
529; connection reset; temporary DNS/network failure; non-cancellation timeout. Do not
normally retry 400/401/402/403/404(bad config)/422; treat 401/402/403 and model-not-found as
config/access failures — disable live NIM for the process (until config reload), warn safely,
use fallback. Honor Retry-After (delta-seconds or HTTP-date; support >10 min; never retry
earlier than requested; a past HTTP-date = no server delay). Local schedule when no valid
Retry-After: ~15,30,60,120,240,480,720,900 s, plus nonnegative jitter up to ~20%, capped at
NVIDIA_MAX_RETRY_DELAY_SECONDS; when Retry-After is valid use the greater of server delay and
capped local delay (do not cap a valid server delay). Honor a delay only within the remaining
NVIDIA_MAX_RETRY_ELAPSED_SECONDS budget (elapsed includes HTTP attempts + local + server
waits); if it exceeds the budget, open the circuit and use fallback rather than violating
Retry-After. Use TimeProvider/monotonic time so waits/countdowns are testable. During waits
keep the UI responsive showing player name/color, a safe category (e.g. "Service busy"),
attempt number, time to next retry, total elapsed, that the game is active, and the QUIT
button. QUIT must promptly cancel an in-flight request or a 10-30 min wait (fake-time tests
verify cancellation without waiting out the delay). After the budget is exhausted, pick a
fallback move and continue; open the circuit for NVIDIA_CIRCUIT_BREAKER_SECONDS; then allow
one half-open probe on a later eligible turn; close on success or reopen on transient failure.

11. AVALONIA UI — polished, responsive, and fully automatic once START GAME is pressed.
11.0 BRANDING (mandatory, both screens): the top-left header AND the window title read
exactly "SANYALnet Labs Ludo AI Arena". A small, unobtrusive copyright notice is fixed at the
BOTTOM-RIGHT of both the setup and game screens, reading exactly "(C) Supratim Sanyal + AI
Agents" (the © glyph is acceptable). Neither the header nor the copyright may overlap or shift
the board or status panels at any window size.
11.1 Setup screen: header/title as above; "Four AI players" subtitle; editable Red/Green/
Yellow/Blue names; the four strategy hints; rules-profile name + Rules button; NVIDIA key-
present status (without the key); NVIDIA model+endpoint status; a warning when fallback will
be used; START GAME and QUIT; bottom-right copyright. No human/player-count controls.
11.2 Game screen: a square central Ludo board; four player status cards; an animated die area;
current phase, active player, revealed value, consecutive-six count, bonus-roll status; NIM
request/retry/fallback/circuit status; a concise scrollable event log; an always-visible QUIT;
bottom-right copyright. Each card shows name, color, strategy, active indicator, tokens in
yard/on track/in home lane/finished, current AI state, and whether the last move came from NIM
or fallback (do not rely on color alone).
11.3 Board rendering: Avalonia vector drawing (no bitmap board); correct 15x15 geometry — four
yards, 52 shared cells, colored start cells, star safe cells, four five-cell home lanes, center
triangles — consuming the authoritative coordinate table. Separate logical positions from pixel
coordinates. LAYOUT STABILITY (mandatory): the board keeps a fixed, square size and position
throughout a game; NO other element (die highlight, status text, event log growth, retry
countdown) may resize or reflow the board. Reserve fixed space for the die/status area so its
content changes never change the board's measured size. Support high DPI; cache brushes/pens/
text/paths where practical; no clipping/overlap at compact or large sizes. Tokens have
outlines, subtle shadows, and a number/shape id; offset multiple tokens on one safe cell;
distinguish active/moving/finished/captured without relying on color alone.
11.4 Animation.
  Die: correct pip layouts 1-6; a polished roll/tumble/bounce/easing; rapid cosmetic
  intermediate faces; normal duration ~700-1100 ms; final face exactly equals the stored
  value; no UI-thread blocking; a reduced-motion mode that still visibly reveals a roll.
  DIE HIGHLIGHT — FLASH, NEVER RESIZE (mandatory): to emphasize the revealed number, FLASH the
  die display (e.g. a brief 2-3x opacity/background/foreground pulse) WITHOUT changing the die
  control's size, width, height, measured bounds, or layout slot. Never enlarge the die to
  show the result — that reflows the board and makes it jitter. If any scale effect is used it
  must be a render-only RenderTransform (ScaleTransform) that does not trigger layout, inside a
  fixed-size container; the preferred solution is a non-resizing flash.
  Token movement — SMOOTH GLIDE, NEVER TELEPORT (mandatory): on every move the piece visibly
  travels from its origin to its destination by INTERPOLATING between consecutive cell centres
  (several sub-steps per cell), so it glides rather than jumps. This applies to EVERY color and
  EVERY move type, INCLUDING single-cell entry moves (yard->start) and home-lane/finishing
  moves — none may teleport. Pace is relaxed: about 300-350 ms per cell. While a piece is in
  flight it must carry a gentle PULSING highlight (e.g. a ring/glow at a few Hz, not a rapid
  flash) so the viewer can follow which piece is moving; captures animate the captured token
  back to its yard. Provide brief tasteful effects for entry, capture, safe landing, blockade,
  home-lane entry, finish, and victory; a configurable animation-speed multiplier; and a small
  inter-turn pause.
  IMPLEMENTATION NOTE (custom-drawn board): if the board is a custom Control, it MUST be
  invalidated/redrawn on EVERY animation frame (e.g. subscribe to the token collection's change
  notifications, or bump a repaint trigger, then call InvalidateVisual) — do not rely on
  incidental repaints from other bindings, or the glide will not render.
11.5 Quit/game-over: in-game QUIT confirms, then cancels animations/HTTP/retry waits/
countdowns/queued requests/state machine before closing; dispose resources; no unobserved task
exceptions. On victory, stop normal processing and show the winner's name/color, final stats, a
restrained celebration, NEW GAME, and QUIT. NEW GAME returns to setup, retains safe name/config
values, creates fresh game/AI-session identifiers, and resets all state. No NEW GAME control
during an active game.
11.6 Accessibility: keyboard navigation, visible focus, Enter/Space activation, accessible
control names, text in addition to color, readable contrast, scalable text, reduced motion, no
rapid flashing, clear disabled states, and safe user-facing errors (no raw stack traces).

12. CONCURRENCY, SECURITY, LOGGING
CancellationToken through the game loop, animation, NIM client, retry, gate, countdown, and
shutdown; never block the UI thread; prevent overlapping rolls/turns/animations/decisions/move-
application. Only one authoritative command resolves a roll; an illegal or stale move fails with
no mutation; the game-over transition is atomic. Structured logging for startup/shutdown, game/
turn ids, state transitions, die values, animation completion, legal-move count, safe AI request
metadata, HTTP status, retry delay, circuit state, fallback, moves, captures, finishes, winner,
cancellation, exceptions. Never log the key, Authorization header, full environment, raw prompts
by default, or unsanitized model output; redact secrets; never execute model-provided code.

13. TESTS — deterministic, never call live NIM, never real-time sleep for long delays; use fake
HTTP handlers, scripted IDieRoller, seeded RNG, fake TimeProvider, fake animation completion.
All test projects MUST compile and pass.
13.1 Core rules: fixed four-player order; animated roll-off + tied rerolls; every offset/route
rotation/shared coordinate/home-lane coordinate/center transition; 52 shared + 5 home + progress
57; safe indices 0,8,13,21,26,34,39,47; six-to-enter and entry at progress 0; entry-vs-move
choice; exact move and overshoot rejection; capture+return; safe immunity and mixed-color safe
occupancy; blockade create/passage/split/own-passage/opponent-block/third-token-rejection; six/
capture/finish bonus and non-stacking; consecutive-six reset after a non-six bonus continuation;
third-six forfeit; no-legal-move on six and non-six; exact finish, finished-token immobility,
immediate victory, no turn after victory; full new-game reset.
13.2 Sequence/invariant: value generated before animation; AI cannot receive it before
completion+reveal; legal moves/NIM only after reveal; every bonus gets a new value + full
animation; no-move and third-six make no NIM request; token animation begins only after
validation; cancellation during animation/HTTP/retry/movement stops safely; stale ids rejected;
one valid location per token; no token in another color's home lane; finished tokens never move;
no illegal stacks/overshoots; illegal commands do not mutate; only the active player acts; at
most one winner.
13.3 Simulation/autoplay: >=1,000 deterministic four-player fallback games over multiple seeds,
invariants checked after each move, each ending with one winner within a documented generous
safety limit (a limit hit is a failure to investigate, not a draw); record count, seed range,
max/avg rolls, winner distribution, failures. Plus an accelerated app-level autoplay (fake
animation, scripted/fallback AI) driving setup->roll-off->turns->bonus->movement->victory->game-
over->NEW GAME with no gameplay input.
13.4 NIM/resilience: correct base URL, PATH-PRESERVING full request URL (assert the effective
URL is .../v1/chat/completions, i.e. "/v1" is not dropped), bearer auth, model, JSON request;
ENVELOPE PARSING — given a realistic {"choices":[{"message":{"content":"{...moveId...}"}}]}
body, the parser extracts choices[0].message.content and returns the correct move (a regression
test for both prior defects); secret redaction; four isolated sessions; request only after
reveal incl. one forced move; valid/fenced/malformed JSON, unknown MoveId, one repair, fallback
after repair; safe reason rendering + length cap; transient retry for 408/425/429/500/502/503/
504/529; no ordinary retry for 400/401/402/403/bad-404/422; Retry-After delta and HTTP-date; a
simulated 12-min Retry-After; backoff progression, nonnegative jitter, per-delay cap, total-time
cap, and Retry-After exceeding the budget; single-flight, min interval, gate release, circuit
open/half-open/close/reopen; cancellation during HTTP and a fake 12-min wait; stale-response
rejection after shutdown or new game.
13.5 Avalonia headless/rendered-frame: use the supported headless integration; enable Skia for
pixel tests. Cover setup, game start, roll-off, active player, mid-roll, final die face, AI
waiting, retry countdown, board layout, token offsets, capture, home lane, game over, NEW GAME,
QUIT, keyboard nav, reduced motion, compact and large windows. Additionally assert: the header
text "SANYALnet Labs Ludo AI Arena" is present; the bottom-right copyright is present; the
board's size/position is UNCHANGED before/during/after a die highlight (layout-stability test);
and a token move visits intermediate cells (smooth-travel test, e.g. via animation keyframes or
sampled positions). Save representative PNGs and inspect pixels (board squareness, alignment,
center geometry, pips, text, no token clipping, safe stars, cards, active indicator, retry
display, QUIT). If no agent can actually view images, say so; do not label metadata-only checks
as visual inspection.
13.6 Optional live NIM smoke test: normal tests never call NVIDIA; permit one opt-in synthetic
decision only when ALLOW_LIVE_NIM_TEST=1 and valid inherited config exist — strict timeout, tiny
legal-move list, no full game, redacted logs.

14. BUILD, RUN, DELIVERY VERIFICATION
Create executable scripts/launch-ludo.sh (strict shell options; locate the solution relative to
itself; verify dotnet and .NET 10; preserve inherited env; never print secrets; launch the app;
return its exit status; handle Ctrl+C). Run and record real output + exit code of:
  dotnet restore LudoNimArena.sln
  dotnet build LudoNimArena.sln -c Release --no-restore
  dotnet test LudoNimArena.sln -c Release --no-build
  dotnet publish src/LudoNimArena.App/LudoNimArena.App.csproj -c Release -r linux-x64 --self-contained false
GUI smoke test: timeout 30s ./scripts/launch-ludo.sh (or timeout 30s xvfb-run -a ./scripts/
launch-ludo.sh if no display). A deliberate timeout counts only if logs prove the app started
without a startup exception and stayed alive to the timeout. Never call an unknown/failed
timeout a success.

15. DOCUMENTATION (must match delivered code, commands, defaults, counts)
README.md: scope, four-AI limitation, features, prerequisites, build, run, env vars, NIM+
fallback behavior, test commands, structure, screenshots/rendered artifacts, security, known
limitations; state human players are a future revision. RULES.md: exact track, offsets, safe
indices, routes, entry, movement, captures, safe occupancy, blockades, three sixes, non-stacking
bonuses, home entry, exact finish, victory. ARCHITECTURE.md: project boundaries, state machine,
roll-before-AI sequence, RNG/time abstractions, command/event flow, per-player sessions, request
identity, retry/circuit flow, cancellation, rendering, the future human-controller extension
point, AND the NIM URL/response-parsing contract from section 8.2. TESTING.md: unit tests,
simulations, fake time/HTTP, headless UI, rendered pixels, optional live NIM test, reproducing
failures. manual.md: names, starting, automatic turns, rules, NIM variables, fallback, long
retry countdowns, QUIT, game over, NEW GAME, logs, troubleshooting. BUILD_NOTES.md: inspected
environment, SDK + pinned package versions, commands executed, actual results, test totals,
simulation statistics, smoke-test result, rendered-frame checks, defects found and corrected,
honest limitations. Never fabricate counts or successful checks.

16. FINAL REVIEW AND ACCEPTANCE
Separate product, rules, sequence, concurrency, resilience, security, visual, and documentation
reviews; correct findings, rerun affected tests, repeat; completion requires two successive full
passes with no new defect. Complete only when all applicable items are true:
- Exactly four autonomous AI players; no human-play controls. C#/net10.0; no Python product.
- The whole solution builds with 0 errors; ALL test projects compile; Release build and tests
  pass (or exact unresolvable environment blockers are reported without false success).
- Headers on both screens read "SANYALnet Labs Ludo AI Arena"; the bottom-right copyright
  "(C) Supratim Sanyal + AI Agents" appears on both screens.
- The board is correctly rendered, responsive, and NEVER changes size/position during play
  (die highlight flashes, does not resize; nothing reflows the board).
- Every roll-off/normal/bonus roll is visibly animated; the AI receives the value only after
  animation completion + reveal.
- Tokens ALWAYS glide smoothly (interpolated, relaxed ~300-350 ms/cell) for every color and
  every move type incl. single-cell entry moves — never teleporting; the in-flight piece is
  visibly highlighted while it travels (verified).
- Legal moves are generated/validated only by the engine; each color has an isolated session
  and every eligible roll gets its own NIM request.
- The NIM request URL preserves "/v1" (no 404) and the response parser reads
  choices[0].message.content; with a valid key+model the game demonstrably makes REAL NIM-driven
  moves (not 100% fallback), with a visible per-player decision-source indicator.
- Game runtime uses a small free-tier model (default nvidia/nemotron-mini-4b-instruct) at
  temperature 0 / max_tokens ~64, spaces requests >= 3 s apart (<= ~20 RPM), retries at most
  2-3 times within ~45 s, then uses the authoritative local fallback; it never freezes on the
  network.
- NIM failure, HTTP 529, Retry-After >10 min, cancellation, and circuit breaking work without
  freezing the UI; local fallback can finish a full game.
- >=1,000 deterministic simulations pass all invariants.
- QUIT works during normal play, die animation, token animation, HTTP, and long retry waits;
  victory stops play and offers NEW GAME and QUIT.
- Rendered-frame checks were performed and accurately reported; docs match code/commands/
  defaults/test counts; no credential/secret appears anywhere.
End with a concise report: (1) project location; (2) implemented features; (3) confirmation of
four AI players and no human mode; (4) build/publish results; (5) test totals + simulation
stats; (6) GUI and rendered-frame verification (incl. real-NIM-move demonstration, header,
copyright, board-stability, smooth-travel); (7) exact launch command; (8) required NVIDIA env
var names (never values); (9) honest remaining limitations.

That prompt is deliberately prescriptive. A weaker model needs the guardrails; a stronger one still benefits from them. Note in particular the sections on the NIM request URL and response parsing—they exist because an earlier run got both subtly wrong, which we will come back to.

The free-model gauntlet

Running an agentic coding loop on a free inference endpoint is a bit like repairing a warp drive with a blunt spoon and high hopes. Here is how each contestant lost.

Nemotron Ultra 550B — the most powerful model, starved

Reaching for the largest model first felt obvious and was completely wrong. Across a full lifecycle run the endpoint returned thousands of HTTP 429 and 529 "please wait" responses; only a handful of calls actually got through. The workflow produced a rich-looking solution it then could not repair, because it never got enough successful requests to iterate.

Lesson

The most powerful model on the menu is useless if you cannot get requests through the door. Free-tier rate limits punish exactly the long, chatty agent loops that a full build requires.

Nemotron super 49B — reliable, but not capable enough

The 49B was the opposite: fast, and it never rate-limited or hung. It also under-built. The "game" it delivered was essentially the default Avalonia template—a "Welcome to Avalonia" window with no board, no tokens, no engine worth the name. Reliable availability is necessary but not sufficient; the model still has to be strong enough for a large C#/Avalonia specification.

Nemotron super 120B — capable and reliable, then it vanished

The 120B was the sweet spot on paper, and for hours it behaved like it: hundreds of successful calls, zero errors, a real MVVM structure with a genuine engine and AI client. Then, mid-run, one model call simply never returned. The connection sat open and idle; the provider's read timeout never fired; the process waited forever on a response that was not coming. I reproduced it on a second run. Same silent hang.

Lesson

Good retry and backoff logic cannot save you from a silently hung connection. A keep-alive-style stall slips past a read timeout, and a workflow with excellent 429 handling will still sit and wait until the heat death of the universe. You need a hard, per-request wall-clock timeout—which most clients do not enable by default.

poolside Laguna XS 2.1 — wrote the code, sabotaged its own build

Laguna is a coding specialist and, at the API level, the most reliable free model of the lot: hundreds of calls with essentially no rate-limiting. It wrote a real, if smaller, C#/Avalonia solution—a game engine, a view model, a NIM client, a board renderer, an AI session. And then it lost a two-hour fight with its own build helper.

The helper script it wrote to invoke dotnet reported "dotnet not found" hundreds of times—on a machine where dotnet --version happily prints 10.0.302 and the SDK is squarely on PATH. Every dotnet build therefore "failed," so it never once achieved a successful compile. It looped in its code-completion phase trying to diagnose a problem it had created and could not see, hit the loop cap, and advanced to code review on a non-building solution. Along the way the endpoint threw intermittent HTTP 500s, and finally—like the 120B before it—a model call silently hung and the run wedged for good.

Lesson

Laguna was reliable enough to stay online and capable enough to write the app—but not capable enough to debug its own tooling. "Can write code" and "can operate a build" are different skills, and an autonomous loop needs both.

The honest through-line

Five free attempts, five different defeats—and most of them were infrastructure, not raw intelligence: rate limits, silent connection hangs, endpoint 500s. The free NVIDIA build catalog is a genuinely wonderful place to prototype, and none of these models is bad. But a multi-hour, tool-calling, self-correcting build is about the least forgiving workload you can point at a free serving tier.

The paid turn: DeepSeek V4 Pro

Having exhausted the free bench, I pointed the ChatDev builder at DeepSeek V4 Pro through its OpenAI-compatible API and let the same prompt run.

This is not a subscription

DeepSeek's API is pay-as-you-go against a prepaid balance. There is no monthly plan to cancel and forget. A one-time $10 top-up goes a very long way—as the bill below shows, this entire five-hour build spent about a dollar of it. You are buying tokens, not a relationship.

The difference was immediate and, frankly, boring in the best way. Calls returned in seconds. No rate-limiting. No silent hang. Over roughly five hours the workflow ran the complete software-development lifecycle—implement, complete, review, test, document—to a clean finish, and produced a genuine multi-project C#/Avalonia solution that built with zero errors and launched a real window.

The Ludo AI Arena setup screen: four colored AI players with editable names and personality labels, the rules profile, and a START GAME button.
What DeepSeek delivered on the first finished run: a polished setup screen with four AI players, each with a strategy personality.

The bill

The run's own token accounting, cross-checked against DeepSeek's V4 Pro rate card, comes out like this. Prompt caching did almost all of the heavy lifting—about 97.5% of the input was served from cache at a tiny fraction of the miss price.

Bucket Tokens Rate / 1M Cost
Input — cache hit 59,918,464 $0.003625 $0.22
Input — cache miss 1,535,808 $0.435 $0.67
Output 286,342 $0.87 $0.25
Total 61.74M ≈ $1.13
The whole point

A complete, hours-long, multi-agent build of a compiled desktop application cost about $1.13. A single $10 top-up covers it roughly eight times over. Rates move and prompt-cache ratios vary—at the older pre-discount reference prices the same run would have been closer to $4.50—but the conclusion is stable: this is lunch money, not a line item.

But "done" hid two silent bugs

Here is the part that matters if you take anything operational away from this series. DeepSeek's delivery built cleanly, passed its own tests, and launched. It also had its headline feature—the four AI players—100% broken, and nothing in the green checkmarks told you so.

Watching a live game, every single turn read "Local fallback AI." The NVIDIA models were never actually being consulted. Two independent bugs, each invisible on its own:

  • The request URL dropped its path. The client set its base address to the endpoint including /v1, then issued a request to /chat/completions with a leading slash. In .NET, a leading-slash path resolves from the host root and discards the /v1—so every call hit a 404.
  • The response parser read the wrong field. The reply is an OpenAI-style envelope; the actual move JSON lives inside choices[0].message.content. The code deserialized the whole envelope into the move object, found no moveId, and quietly returned nothing.

Either bug alone forces a fallback. Both were masked because the local fallback AI is good, and because no test exercised a live model round-trip. I fixed the URL construction and the parsing, and the four players came alive—each move now annotated with the model's own one-line reasoning.

The game's event log showing real model-generated move reasoning such as 'Move the first token forward to a safe square', with no 'Local fallback AI' lines.
After the two fixes: the event log fills with real model-chosen moves and their reasoning—no more silent fallback.
Lesson

A green build and a passing suite can conceal a headline feature that is silently, completely broken. The org chart of agents will happily sign off on it. The final authority is always executable evidence you gathered yourself—here, watching the actual game and reading the actual log—not managerial optimism, artificial or otherwise.

Polish

With the game genuinely model-driven, the remaining work was ordinary product finishing: a consistent header and a copyright line, tokens that glide smoothly between cells instead of teleporting (with a gentle pulsing highlight on the piece in motion), and a die that flashes to show its result without resizing and shoving the whole board around on every roll. None of this is dramatic. All of it is the difference between "the agents produced something" and "this is a program a person would use."

One complete game, no human moves: four autonomous AI players take the arena from the opening roll-off to the winning token, with animated dice, gliding pieces and model-selected moves throughout. Watch it directly on YouTube.

Why keep the runtime model small and separate

For actual gameplay I did not reuse the expensive builder. The move-picker is a narrow task—read a compact board state, choose one legal moveId, return tiny JSON—so a small, fast, free model is the right tool, backed by the local deterministic AI whenever the endpoint blinks. That is the division of labor again: a capable paid model to construct the software, a cheap free model to play, and deterministic local code to guarantee the game never freezes waiting on the network.

Build it yourself, end to end

This is the copy-paste path a newcomer can follow from the end of Part 1 to the finished, playable game. It assumes ChatDev is installed in ~/dev/ChatDev and your free NVIDIA NIM key already works, exactly as Part 1 left it. The one new idea is that this project uses two model providers for two different jobs:

  • DeepSeek V4 Pro (paid, ~$1 for the whole run) builds the game through ChatDev's agents.
  • Your free NVIDIA NIM key from Part 1 plays the game at runtime — the four tokens consult a small free model, backed by a local fallback AI.
STEP 01

Get a DeepSeek API key and top up $10

Sign in at platform.deepseek.com, open API keys, and create one (it begins sk-). Then add a one-time $10 top-up under Billing. This is pay-as-you-go against a prepaid balance—not a subscription. As the bill earlier in this article shows, the entire five-hour build spent about $1.13, so ten dollars lasts a long time.

STEP 02

Point ChatDev at DeepSeek

ChatDev reads the build provider from .env. Back up your current (GLM) settings, then edit the two lines that matter:

cd ~/dev/ChatDev
cp .env .env.glm.bak
nano .env

Set these two values (leave the rest of the file alone), then save and exit:

BASE_URL=https://api.deepseek.com
API_KEY=sk-XXXXXXXXXXXXXXXXXXXX        # your DeepSeek key

Now select the builder model in the workflow. Restore the shipped file from the backup Part 1 made, then swap in DeepSeek V4 Pro on all nine agent nodes:

cp yaml_instance/ChatDev_v1.yaml.bak yaml_instance/ChatDev_v1.yaml
sed -i 's|name: gpt-4o|name: deepseek-v4-pro|g' yaml_instance/ChatDev_v1.yaml
grep -c 'name: deepseek-v4-pro' yaml_instance/ChatDev_v1.yaml   # expect: 9
Why a different provider than Part 1

A DeepSeek key only serves DeepSeek's own models, so its base URL is api.deepseek.com and the model is deepseek-v4-pro. This provider is used only for the build. The finished game's players use your NVIDIA NIM key from Part 1 instead (Step 07), so keep that key handy—your .env.glm.bak still has it.

STEP 03

Install the .NET 10 SDK

Unlike the Python projects in Parts 1 and 2, this product is C# on .NET 10. The agents compile and test it during the build, and you run it afterwards, so the SDK must be present before you start the run. Install it into your home directory:

curl -fsSL https://dot.net/v1/dotnet-install.sh -o /tmp/dotnet-install.sh
bash /tmp/dotnet-install.sh --channel 10.0 --install-dir "$HOME/.dotnet"

export DOTNET_ROOT="$HOME/.dotnet"
export PATH="$DOTNET_ROOT:$PATH"
dotnet --version        # expect: 10.0.x
Note

Add the two export lines to your ~/.bashrc so every future shell—including the build session in Step 05—can find dotnet.

STEP 04

Save the requirements prompt

Put the full specification shown earlier (the scrollable box under “The requirements prompt”) into a file, then verify it saved intact:

cd ~/dev/ChatDev
mkdir -p prompts
nano prompts/ludo-nim-ai-arena-prompt.md   # paste the whole spec, save, exit

wc -c      prompts/ludo-nim-ai-arena-prompt.md   # expect ~37589 bytes
sha256sum  prompts/ludo-nim-ai-arena-prompt.md
Prompt integrity

The reference spec is 37,589 bytes, SHA-256 3e51e235d51b3ed01a7db5c2d2a511deaa9a14e62c7f1885d0288c34b3d74f6a. A byte or two of difference is just a trailing newline and is harmless; the point is that the whole spec pasted without truncation.

STEP 05

Run the full build in a detached session

This is a multi-hour run, so start it under screen and pipe a short task in alongside the attached spec:

command -v screen || sudo apt-get install -y screen

screen -dmS LudoNimArena bash -lc '
cd "$HOME/dev/ChatDev"
export PATH="$HOME/.local/bin:$PATH"
export DOTNET_ROOT="$HOME/.dotnet"; export PATH="$DOTNET_ROOT:$PATH"
printf "%s\n" \
  "Build the complete SANYALnet Labs Ludo AI Arena desktop game exactly as specified in the attached requirements document. The product language is C# on .NET 10 with Avalonia UI; reject any Python product implementation. Execute the full software-development lifecycle: implement, build with dotnet, review, test, render and visually inspect the running application, correct every defect, and document the result. Continue until two successive full review passes find no new defect." \
| uv run python run.py \
    --path yaml_instance/ChatDev_v1.yaml \
    --name LudoNimArena \
    --attachment prompts/ludo-nim-ai-arena-prompt.md
'

Watch it whenever you like, and detach again without stopping it:

screen -r LudoNimArena        # attach; press Ctrl+A then D to detach
What to expect

This run took about five hours and spent roughly $1.13 of DeepSeek credit. Prompt caching does almost all of the work, so let the one session run to completion rather than restarting it.

STEP 06

Locate and copy the finished game

ChatDev writes each run under WareHouse/ in a timestamped folder. Grab the newest, confirm it contains the game, and copy the source into a clean operating directory—~/dev/LudoNimArena, mirroring how Part 2 used ~/dev/LiveNewsDebateWall:

cd ~/dev/ChatDev

RUN=$(ls -dt WareHouse/LudoNimArena_* | head -1)
echo "$RUN"
test -f "$RUN/code_workspace/src/LudoNimArena.App/LudoNimArena.App.csproj" \
  && echo "game found" || echo "no game yet — check the run log"

mkdir -p ~/dev/LudoNimArena
rsync -a \
  --exclude='.venv/' --exclude='obj/' --exclude='bin/' \
  --exclude='__pycache__/' --exclude='attachments/' \
  "$RUN/code_workspace/" ~/dev/LudoNimArena/
Your build will not be identical to mine

ChatDev is non-deterministic—file names and structure vary from run to run. The spec above (section 8.2) now explicitly forbids the two model-integration bugs I hit and requires regression tests for them, so a fresh build should wire the model up correctly from the start. Always confirm with a live game in the next step; if every move reads “Local fallback AI,” it is almost always the model pin, explained there.

STEP 07

Build and run the game

Compile the Avalonia app once:

cd ~/dev/LudoNimArena
export DOTNET_ROOT="$HOME/.dotnet"; export PATH="$DOTNET_ROOT:$PATH"
dotnet build src/LudoNimArena.App -c Release

Then launch it. The players use your free NVIDIA NIM key from Part 1 (not DeepSeek), and you must pin a small, fast, non-reasoning move model:

export NVIDIA_API_KEY=nvapi-XXXXXXXXXXXX           # your NIM key from Part 1
export NVIDIA_MODEL='nvidia/nemotron-mini-4b-instruct'
export NVIDIA_CIRCUIT_BREAKER_SECONDS=5

dotnet run --project src/LudoNimArena.App -c Release

The window opens on the Linux desktop; click START GAME and the four players start consulting the model.

If every move says "Local fallback AI"

The game's built-in default model is a reasoning model, and the move request uses a small token budget. A reasoning model spends that whole budget thinking and returns an empty answer, so the parser gets nothing and falls back on every turn. Pinning a non-reasoning model such as nvidia/nemotron-mini-4b-instruct (as above) fixes it. The NVIDIA_CIRCUIT_BREAKER_SECONDS=5 matters too—the built-in default is 300 seconds, so one early hiccup would otherwise keep you on fallback for five full minutes.

STEP 08

Customize the players

The four names are editable on the setup screen every game, but you can bake in your own defaults so they are pre-filled on every launch. To give the arena a cult-sci-fi roster—HAL 9000, Marvin, Mal and Deckard—edit the default values and their matching watermark hints, then rebuild:

cd ~/dev/LudoNimArena

# default names shown pre-filled on the setup screen
sed -i \
  -e 's/_redName = "Red AI"/_redName = "HAL 9000"/' \
  -e 's/_greenName = "Green AI"/_greenName = "Marvin"/' \
  -e 's/_yellowName = "Yellow AI"/_yellowName = "Mal"/' \
  -e 's/_blueName = "Blue AI"/_blueName = "Deckard"/' \
  src/LudoNimArena.App/MainViewModel.cs

# matching watermark hints (shown if a name box is cleared)
sed -i \
  -e 's/Watermark="Red AI"/Watermark="HAL 9000"/' \
  -e 's/Watermark="Green AI"/Watermark="Marvin"/' \
  -e 's/Watermark="Yellow AI"/Watermark="Mal"/' \
  -e 's/Watermark="Blue AI"/Watermark="Deckard"/' \
  src/LudoNimArena.App/MainWindow.axaml

dotnet build src/LudoNimArena.App -c Release
Adjust to your build

In this build the names lived in MainViewModel.cs as _redName = "Red AI" and as the setup boxes' Watermark attributes in MainWindow.axaml. Since your generated code may name these slightly differently, open the two files and adjust the patterns to match what you actually see. Relaunch and the setup screen opens with the new lineup—still editable per game. The event log reads accordingly: “HAL 9000 captured Marvin,” “Mal rolled a six,” “Deckard reached home.”

When it goes sideways

dotnet: command not found

Cause: the .NET 10 SDK isn't on this shell's PATH.

Fix: complete Step 03, then export DOTNET_ROOT="$HOME/.dotnet"; export PATH="$DOTNET_ROOT:$PATH" (and add both lines to ~/.bashrc).

Every move in the event log says "Local fallback AI"

Cause: the built-in default is a reasoning model that returns empty content under the small move-token budget, so the game falls back on every turn.

Fix: pin a non-reasoning model and shorten the breaker, as in Step 07: export NVIDIA_MODEL='nvidia/nemotron-mini-4b-instruct' and export NVIDIA_CIRCUIT_BREAKER_SECONDS=5. Confirm NVIDIA_API_KEY is exported too.

No WareHouse/LudoNimArena_ folder appears

Cause: the build aborted early—usually a DeepSeek 401 (bad key) or 402 (empty prepaid balance).

Fix: attach with screen -r LudoNimArena, read the error, fix the key or top-up in Step 01/02, and re-run Step 05.

The --project path doesn't exist in your build

Cause: a non-deterministic run named the app project differently.

Fix: find it with find ~/dev/LudoNimArena -name '*.csproj' and point dotnet run --project at the App project it reports.

AI parody notice

The game and its on-screen "reasoning" are fictional and generated by artificial intelligence for demonstration only. Nothing here is a real statement, policy, endorsement or official position of any person, company or institution, including NVIDIA, DeepSeek, poolside or any model provider named above.

Does it really run everywhere? A four-machine test

The whole reason to build this on .NET 10 with Avalonia—rather than a Linux-only toolkit—was portability: one C# codebase, one build command, a native desktop window on every major operating system. A claim like that is worth nothing untested. So I took the exact source the DeepSeek run produced—byte for byte, no per-platform edits—and built and ran it on four machines: the Linux desktop it was born on, a macOS box, a Windows 10 PC, and a Windows 11 laptop.

Result

Identical source, four operating systems, zero code changes and zero build errors on every one. Each brought up the same window—the cross-shaped board, four AI players, the animated die and the live event log—and played a full game.

Platform Runtime Result
Linux desktop .NET 10 Builds clean; plays. Reference platform.
Windows 10 .NET 10 Builds clean; plays in a native window.
Windows 11 .NET 10 Builds clean; plays in a native window.
macOS Big Sur 11 .NET 10.0.302 SDK installs; builds clean; window launches and plays.

The recipe is the same everywhere: install the .NET 10 SDK, copy the LudoNimArena folder produced back in Step 06, and run the App project. Only the shell syntax and the SDK installer differ from one OS to the next.

Linux

This is the platform the runbook above already covers—built and launched with the commands in Step 07. One extra tip for a minimal desktop with no GPU driver: force software rendering so Avalonia's Skia backend paints reliably.

cd ~/dev/LudoNimArena
# only needed on a headless / GPU-less desktop
export LIBGL_ALWAYS_SOFTWARE=1 GALLIUM_DRIVER=llvmpipe
dotnet run --project src/LudoNimArena.App -c Release
SANYALnet Labs Ludo AI Arena running on a Linux desktop: the cross-shaped Ludo board mid-game with HAL 9000, Marvin, Mal and Deckard, and an event log of AI-chosen moves.
Linux: the reference build, well into an auto-played game.

Windows 10 and 11

Install the SDK once (the official installer from dotnet.microsoft.com, or via winget), copy the folder onto the machine, and run the App project from any terminal:

winget install Microsoft.DotNet.SDK.10   # or run the downloaded installer
cd %USERPROFILE%\LudoNimArena
dotnet run --project src\LudoNimArena.App -c Release

The setup screen opens; press START GAME. A Windows 10 desktop and a Windows 11 laptop ran the identical binary—same board, same die, same event log—with no changes.

macOS

Big Sur (macOS 11) has no winget/apt equivalent for the SDK, so install .NET 10 with Microsoft's script, then build and run exactly as elsewhere:

curl -fsSL https://dot.net/v1/dotnet-install.sh | bash -s -- --channel 10.0 --install-dir "$HOME/.dotnet"
export DOTNET_ROOT="$HOME/.dotnet"; export PATH="$DOTNET_ROOT:$PATH"
cd ~/LudoNimArena
dotnet run --project src/LudoNimArena.App -c Release

The .NET 10.0.302 SDK installed cleanly on an operating system several years old, the solution built with no errors, and the Avalonia window came up and played—the same board and event log as everywhere else.

SANYALnet Labs Ludo AI Arena on macOS Big Sur: the Avalonia window on the desktop showing the Ludo board, the four AI player cards, a die reading 4, a 'Deckard moving…' status, the bottom-right copyright, and the scrolling event log.
macOS Big Sur: the identical source again, native on the Mac desktop—title bar, board, die and event log all present.
Why this matters

None of this needed a cross-compilation matrix, platform #if blocks, or a second UI. The stack the spec told ChatDev to target—.NET 10 with Avalonia—does the portability work itself: the same managed assemblies plus a native Skia renderer per OS. The agents never touched a Mac or a Windows box during the build; portability came from the framework choice in the requirements, not from any platform-specific coaching.

The result

Unlike Parts 1 and 2, this one does not end with a tidy "and the free stack shipped it." It ends with a genuinely instructive split. The free frontier is a real frontier: model availability comes and goes, connections stall silently, and a model can be smart enough to write your app yet not smart enough to operate its own build. Every free contestant lost, and each lost in a different, informative way.

  • The most powerful free model can be the least usable, if you cannot get requests through.
  • Reliable availability and real capability are separate properties; you need both.
  • Timeouts and retries do not catch a silent connection hang—only a hard wall-clock deadline does.
  • A coding model can pass on writing code and still fail by mismanaging the shell.
  • A capable paid model finished the whole lifecycle—for about a dollar, from a prepaid balance, no subscription.
  • Even a paid "done" needs independent verification; a green build hid a fully broken headline feature.

The larger point is the one that keeps drawing me back to this whole scene. A prepaid ten dollars, a fork of an open-source agent framework, a free model catalog to prototype against, and a compiled desktop game assembled by a virtual software company over an afternoon—this is not the future anyone promised, but it is at least using the CPU, and it feels a great deal like the good old chaotic frontier of computing, back when the fun was in making the machine do one more improbable thing.


Part 1 proved the factory was wired correctly; Part 2 gave it a real specification and got a real application. Part 3 pushed on the one variable those two held fixed—the model—and learned that for a hard, hours-long build, the choice between "free" and "a dollar" is the difference between a colorful loading screen and a program you can actually run.

Useful links: Part 1: Install ChatDev 2.0 on Linux · Part 2: Live AI News Debate Wall · My ChatDev fork · Upstream ChatDev · NVIDIA build catalog · DeepSeek API docs

Model IDs, provider rate limits, endpoint availability and token prices all drift. Recheck the current model cards, the ChatDev repository and the live DeepSeek and NVIDIA pricing if any figure here has moved since publication. Token costs are computed from this run's own usage accounting and the V4 Pro rate card at the time of writing.

No comments:

Post a Comment

"SEO" link builders: move on, your spam link will not get posted.

Note: Only a member of this blog may post a comment.