diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 00000000..8080b015 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,31 @@ +# BitWorld + +## Bot Players — Tournament Compatibility + +Bot players must be compatible with the Coworld tournament runner. The full spec +lives in the metta repo at: + + metta/packages/coworld/src/coworld/GAME_RUNTIME_README.md + +The key requirements for bot containers: + +1. Read `COGAMES_ENGINE_WS_URL` env var — the runner passes the full websocket + URL (including slot and token query params) via this variable. + +2. Accept CLI args `--name`, `--token`, and `--slot` as fallbacks for local + testing and older runner versions. + +3. Connect to the game's `/player` websocket endpoint with `slot`, `token`, and + `name` as query params. + +See `planet_wars/players/skurge/skurge.nim` for a reference implementation. + +`tools/docker_build.nim` validates these requirements before building images. + +## Building and Deploying + +```sh +nim r tools/docker_build.nim --push stag_hunt --bots +``` + +See README.md for full deploy workflow. diff --git a/CLAUDE.md b/CLAUDE.md new file mode 120000 index 00000000..47dc3e3d --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1 @@ +AGENTS.md \ No newline at end of file diff --git a/stag_hunt/players/coordinator/coordinator.nim b/stag_hunt/players/coordinator/coordinator.nim index e703fee3..2f7b5455 100644 --- a/stag_hunt/players/coordinator/coordinator.nim +++ b/stag_hunt/players/coordinator/coordinator.nim @@ -771,13 +771,17 @@ proc addQueryParam(url, key, value: string): string = result.add('=') result.add(value.queryEscape()) -proc connectUrl(address, url, name: string, port: int): string = +proc connectUrl(address, url, name, token: string, port, slot: int): string = ## Builds the player websocket URL. if url.len > 0: result = url.withPath(CoordinatorWebSocketPath) else: result = "ws://" & address & ":" & $port & CoordinatorWebSocketPath result = result.addQueryParam("name", name) + if slot >= 0: + result = result.addQueryParam("slot", $slot) + if token.len > 0: + result = result.addQueryParam("token", token) proc initBot(): Bot = result.selfFound = false @@ -826,10 +830,12 @@ proc runBot( address = DefaultHost, port = CoordinatorDefaultPort, url = "", - name = "coordinator" + name = "coordinator", + token = "", + slot = -1 ) = ## Connects coordinator to Stag Hunt and runs the coalition policy. - let endpoint = connectUrl(address, url, name, port) + let endpoint = connectUrl(address, url, name, token, port, slot) while true: try: echo "coordinator connecting to ", endpoint @@ -852,8 +858,10 @@ when isMainModule: var address = DefaultHost port = CoordinatorDefaultPort - url = "" + url = getEnv("COGAMES_ENGINE_WS_URL") name = "coordinator" + token = "" + slot = -1 for kind, key, value in getopt(): case kind @@ -863,6 +871,8 @@ when isMainModule: of "port": port = parseInt(value) of "url": url = value of "name": name = value + of "token": token = value + of "slot": slot = parseInt(value) else: raise newException(ValueError, "Unknown option: --" & key) of cmdArgument, cmdShortOption: @@ -870,4 +880,4 @@ when isMainModule: of cmdEnd: discard - runBot(address, port, url, name) + runBot(address, port, url, name, token, slot) diff --git a/stag_hunt/players/nearest_hunter/nearest_hunter.nim b/stag_hunt/players/nearest_hunter/nearest_hunter.nim index 086408f3..db68beee 100644 --- a/stag_hunt/players/nearest_hunter/nearest_hunter.nim +++ b/stag_hunt/players/nearest_hunter/nearest_hunter.nim @@ -666,13 +666,17 @@ proc addQueryParam(url, key, value: string): string = result.add('=') result.add(value.queryEscape()) -proc connectUrl(address, url, name: string, port: int): string = +proc connectUrl(address, url, name, token: string, port, slot: int): string = ## Builds the player websocket URL. if url.len > 0: result = url.withPath(WebSocketPath) else: result = "ws://" & address & ":" & $port & WebSocketPath result = result.addQueryParam("name", name) + if slot >= 0: + result = result.addQueryParam("slot", $slot) + if token.len > 0: + result = result.addQueryParam("token", token) proc initBot(): Bot = ## Creates a fresh nearest-hunter bot state. @@ -723,10 +727,12 @@ proc runBot( address = DefaultHost, port = DefaultPort, url = "", - name = "nearest_hunter" + name = "nearest_hunter", + token = "", + slot = -1 ) = ## Connects to a stag_hunt server and pursues the closest visible prey. - let endpoint = connectUrl(address, url, name, port) + let endpoint = connectUrl(address, url, name, token, port, slot) while true: try: echo "nearest_hunter connecting to ", endpoint @@ -749,21 +755,21 @@ when isMainModule: var address = DefaultHost port = DefaultPort - url = "" + url = getEnv("COGAMES_ENGINE_WS_URL") name = "nearest_hunter" + token = "" + slot = -1 for kind, key, value in getopt(): case kind of cmdLongOption: case key - of "address": - address = value - of "port": - port = parseInt(value) - of "url": - url = value - of "name": - name = value + of "address": address = value + of "port": port = parseInt(value) + of "url": url = value + of "name": name = value + of "token": token = value + of "slot": slot = parseInt(value) else: raise newException(ValueError, "Unknown option: --" & key) of cmdArgument, cmdShortOption: @@ -771,4 +777,4 @@ when isMainModule: of cmdEnd: discard - runBot(address, port, url, name) + runBot(address, port, url, name, token, slot) diff --git a/stag_hunt/players/rabbiteer/rabbiteer.nim b/stag_hunt/players/rabbiteer/rabbiteer.nim index 8f450bc8..6e311051 100644 --- a/stag_hunt/players/rabbiteer/rabbiteer.nim +++ b/stag_hunt/players/rabbiteer/rabbiteer.nim @@ -493,16 +493,17 @@ proc addQueryParam(url, key, value: string): string = result.add('=') result.add(value.queryEscape()) -proc connectUrl( - address, url, name: string, - port: int -): string = +proc connectUrl(address, url, name, token: string, port, slot: int): string = ## Builds the player websocket URL for Stag Hunt. if url.len > 0: result = url.withPath(WebSocketPath) else: result = "ws://" & address & ":" & $port & WebSocketPath result = result.addQueryParam("name", name) + if slot >= 0: + result = result.addQueryParam("slot", $slot) + if token.len > 0: + result = result.addQueryParam("token", token) proc initBot(): Bot = ## Creates a fresh rabbiteer bot state. @@ -553,10 +554,12 @@ proc runBot( address = DefaultHost, port = DefaultPort, url = "", - name = "rabbiteer" + name = "rabbiteer", + token = "", + slot = -1 ) = ## Connects rabbiteer to Stag Hunt and chases visible rabbits forever. - let endpoint = connectUrl(address, url, name, port) + let endpoint = connectUrl(address, url, name, token, port, slot) while true: try: echo "rabbiteer connecting to ", endpoint @@ -580,21 +583,21 @@ when isMainModule: var address = DefaultHost port = DefaultPort - url = "" + url = getEnv("COGAMES_ENGINE_WS_URL") name = "rabbiteer" + token = "" + slot = -1 for kind, key, value in getopt(): case kind of cmdLongOption: case key - of "address": - address = value - of "port": - port = parseInt(value) - of "url": - url = value - of "name": - name = value + of "address": address = value + of "port": port = parseInt(value) + of "url": url = value + of "name": name = value + of "token": token = value + of "slot": slot = parseInt(value) else: raise newException(ValueError, "Unknown option: --" & key) of cmdArgument, cmdShortOption: @@ -602,4 +605,4 @@ when isMainModule: of cmdEnd: discard - runBot(address, port, url, name) + runBot(address, port, url, name, token, slot) diff --git a/stag_hunt/players/sidekick/sidekick.nim b/stag_hunt/players/sidekick/sidekick.nim index 5542a1e0..429a4652 100644 --- a/stag_hunt/players/sidekick/sidekick.nim +++ b/stag_hunt/players/sidekick/sidekick.nim @@ -560,12 +560,16 @@ proc addQueryParam(url, key, value: string): string = result.add('=') result.add(value.queryEscape()) -proc connectUrl(address, url, name: string, port: int): string = +proc connectUrl(address, url, name, token: string, port, slot: int): string = if url.len > 0: result = url.withPath(WebSocketPath) else: result = "ws://" & address & ":" & $port & WebSocketPath result = result.addQueryParam("name", name) + if slot >= 0: + result = result.addQueryParam("slot", $slot) + if token.len > 0: + result = result.addQueryParam("token", token) proc initBot(): Bot = result.selfObjectId = -1 @@ -611,9 +615,11 @@ proc runBot( address = DefaultHost, port = DefaultPort, url = "", - name = "sidekick" + name = "sidekick", + token = "", + slot = -1 ) = - let endpoint = connectUrl(address, url, name, port) + let endpoint = connectUrl(address, url, name, token, port, slot) while true: try: echo "sidekick connecting to ", endpoint @@ -635,8 +641,10 @@ when isMainModule: var address = DefaultHost port = DefaultPort - url = "" + url = getEnv("COGAMES_ENGINE_WS_URL") name = "sidekick" + token = "" + slot = -1 for kind, key, value in getopt(): case kind @@ -646,6 +654,8 @@ when isMainModule: of "port": port = parseInt(value) of "url": url = value of "name": name = value + of "token": token = value + of "slot": slot = parseInt(value) else: raise newException(ValueError, "Unknown option: --" & key) of cmdArgument, cmdShortOption: @@ -653,4 +663,4 @@ when isMainModule: of cmdEnd: discard - runBot(address, port, url, name) + runBot(address, port, url, name, token, slot) diff --git a/stag_hunt/players/stag_hunter/stag_hunter.nim b/stag_hunt/players/stag_hunter/stag_hunter.nim index 1e5e8e1e..86622637 100644 --- a/stag_hunt/players/stag_hunter/stag_hunter.nim +++ b/stag_hunt/players/stag_hunter/stag_hunter.nim @@ -444,12 +444,16 @@ proc addQueryParam(url, key, value: string): string = result.add('=') result.add(value.queryEscape()) -proc connectUrl(address, url, name: string, port: int): string = +proc connectUrl(address, url, name, token: string, port, slot: int): string = if url.len > 0: result = url.withPath(WebSocketPath) else: result = "ws://" & address & ":" & $port & WebSocketPath result = result.addQueryParam("name", name) + if slot >= 0: + result = result.addQueryParam("slot", $slot) + if token.len > 0: + result = result.addQueryParam("token", token) proc initBot(): Bot = result.selfObjectId = -1 @@ -490,9 +494,11 @@ proc runBot( address = DefaultHost, port = DefaultPort, url = "", - name = "stag_hunter" + name = "stag_hunter", + token = "", + slot = -1 ) = - let endpoint = connectUrl(address, url, name, port) + let endpoint = connectUrl(address, url, name, token, port, slot) while true: try: echo "stag_hunter connecting to ", endpoint @@ -515,8 +521,10 @@ when isMainModule: var address = DefaultHost port = DefaultPort - url = "" + url = getEnv("COGAMES_ENGINE_WS_URL") name = "stag_hunter" + token = "" + slot = -1 for kind, key, value in getopt(): case kind @@ -526,6 +534,8 @@ when isMainModule: of "port": port = parseInt(value) of "url": url = value of "name": name = value + of "token": token = value + of "slot": slot = parseInt(value) else: raise newException(ValueError, "Unknown option: --" & key) of cmdArgument, cmdShortOption: @@ -533,4 +543,4 @@ when isMainModule: of cmdEnd: discard - runBot(address, port, url, name) + runBot(address, port, url, name, token, slot) diff --git a/tools/docker_build.nim b/tools/docker_build.nim index fcbdd07f..454dbc1d 100644 --- a/tools/docker_build.nim +++ b/tools/docker_build.nim @@ -409,6 +409,40 @@ proc addBotTargets( if target.supportsGame(game.name): result.addUniqueTarget(target) +const + TournamentArgs = ["name", "token", "slot"] + TournamentEnv = "COGAMES_ENGINE_WS_URL" + +proc checkTournamentArgs(root: string, targets: openArray[DockerTarget]) = + ## Verifies bot source files accept --name, --token, --slot, and the env var. + var failed = false + for target in targets: + if target.isGame: + continue + let nimFile = target.contextDir / target.name & ".nim" + if not fileExists(nimFile): + continue + let source = readFile(nimFile) + if "getopt" notin source: + continue + for arg in TournamentArgs: + if ("of \"" & arg & "\"") notin source: + echo "Error: ", nimFile.relativePath(root), + " does not handle --", arg, + " (required for tournaments)" + failed = true + if TournamentEnv notin source: + echo "Error: ", nimFile.relativePath(root), + " does not read ", TournamentEnv, + " (required for tournaments)" + failed = true + if failed: + echo "" + echo "Bot players must accept --name, --token, --slot and read" + echo TournamentEnv, " to work in tournaments." + echo "See metta/packages/coworld/src/coworld/GAME_RUNTIME_README.md" + quit(1) + proc ensureBuildx() = ## Verifies that docker buildx is available. let (output, code) = execCmdEx("docker buildx version") @@ -635,6 +669,8 @@ proc main() = echo " bots: ", includeBots echo " targets: ", targetNames(chosen) + checkTournamentArgs(root, chosen) + ensureBuildx() if push or "," in platforms: ensureBuilder()