clawtan 0.1.13 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/clawtan/cli.py +97 -50
- package/package.json +1 -1
package/clawtan/cli.py
CHANGED
|
@@ -3,22 +3,21 @@
|
|
|
3
3
|
clawtan -- CLI for AI agents playing Settlers of Clawtan.
|
|
4
4
|
|
|
5
5
|
Every command prints structured text to stdout designed for easy scanning
|
|
6
|
-
by LLM agents.
|
|
7
|
-
|
|
8
|
-
CLAWTAN_SERVER Server URL (default: http://localhost:8000)
|
|
9
|
-
CLAWTAN_GAME Game ID
|
|
10
|
-
CLAWTAN_TOKEN Auth token from join
|
|
11
|
-
CLAWTAN_COLOR Your player color
|
|
6
|
+
by LLM agents. Session credentials are saved automatically on join.
|
|
12
7
|
|
|
13
8
|
Typical agent flow:
|
|
14
|
-
clawtan quick-join --name "LobsterBot"
|
|
15
|
-
export CLAWTAN_GAME=... CLAWTAN_TOKEN=... CLAWTAN_COLOR=...
|
|
9
|
+
clawtan quick-join --name "LobsterBot" # session saved to ~/.clawtan_session
|
|
16
10
|
clawtan board # once, to learn the map
|
|
17
11
|
clawtan wait # blocks until your turn
|
|
18
12
|
clawtan act ROLL_THE_SHELLS
|
|
19
13
|
clawtan act BUILD_TIDE_POOL 42
|
|
20
14
|
clawtan act END_TIDE
|
|
21
15
|
clawtan wait # next turn...
|
|
16
|
+
|
|
17
|
+
Session lookup order (per field):
|
|
18
|
+
1. CLI flags (--game, --token, --color)
|
|
19
|
+
2. Environment variables (CLAWTAN_GAME, CLAWTAN_TOKEN, CLAWTAN_COLOR)
|
|
20
|
+
3. Session file (~/.clawtan_session, override with CLAWTAN_SESSION_FILE)
|
|
22
21
|
"""
|
|
23
22
|
|
|
24
23
|
import argparse
|
|
@@ -125,13 +124,47 @@ def _get(path, token=None):
|
|
|
125
124
|
|
|
126
125
|
|
|
127
126
|
# ---------------------------------------------------------------------------
|
|
128
|
-
#
|
|
127
|
+
# Session file helpers
|
|
128
|
+
# ---------------------------------------------------------------------------
|
|
129
|
+
def _session_path() -> str:
|
|
130
|
+
return os.environ.get("CLAWTAN_SESSION_FILE") or os.path.expanduser("~/.clawtan_session")
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def _save_session(game_id: str, token: str, color: str):
|
|
134
|
+
path = _session_path()
|
|
135
|
+
data = {"GAME": game_id, "TOKEN": token, "COLOR": color}
|
|
136
|
+
try:
|
|
137
|
+
with open(path, "w") as f:
|
|
138
|
+
json.dump(data, f)
|
|
139
|
+
except OSError as e:
|
|
140
|
+
print(f"Warning: could not save session to {path}: {e}", file=sys.stderr)
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def _load_session() -> dict:
|
|
144
|
+
path = _session_path()
|
|
145
|
+
try:
|
|
146
|
+
with open(path) as f:
|
|
147
|
+
return json.load(f)
|
|
148
|
+
except (OSError, json.JSONDecodeError):
|
|
149
|
+
return {}
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
# ---------------------------------------------------------------------------
|
|
153
|
+
# Environment / session variable helpers
|
|
129
154
|
# ---------------------------------------------------------------------------
|
|
130
155
|
def _env(name: str, arg_val=None, required=True):
|
|
131
|
-
|
|
156
|
+
# 1. CLI flag
|
|
157
|
+
val = arg_val
|
|
158
|
+
# 2. Environment variable
|
|
159
|
+
if not val:
|
|
160
|
+
val = os.environ.get(f"CLAWTAN_{name}")
|
|
161
|
+
# 3. Session file
|
|
162
|
+
if not val:
|
|
163
|
+
val = _load_session().get(name)
|
|
132
164
|
if required and not val:
|
|
133
165
|
print(
|
|
134
|
-
f"ERROR: Missing {name}. Pass --{name.lower()}
|
|
166
|
+
f"ERROR: Missing {name}. Pass --{name.lower()}, set CLAWTAN_{name},"
|
|
167
|
+
f" or run 'clawtan quick-join' to create a session.",
|
|
135
168
|
file=sys.stderr,
|
|
136
169
|
)
|
|
137
170
|
sys.exit(1)
|
|
@@ -290,10 +323,8 @@ def _print_opponents(opponents: list):
|
|
|
290
323
|
|
|
291
324
|
_ACTION_HINTS = {
|
|
292
325
|
"RELEASE_CATCH": (
|
|
293
|
-
"Discard
|
|
294
|
-
" CLI: clawtan act RELEASE_CATCH
|
|
295
|
-
" Or pick specific cards (freqdeck=[DRIFTWOOD,CORAL,SHRIMP,KELP,PEARL]):\n"
|
|
296
|
-
" CLI: clawtan act RELEASE_CATCH '[1,0,0,1,0]'"
|
|
326
|
+
"Discard half your cards (server selects randomly).\n"
|
|
327
|
+
" CLI: clawtan act RELEASE_CATCH"
|
|
297
328
|
),
|
|
298
329
|
"MOVE_THE_KRAKEN": (
|
|
299
330
|
"Move robber: value = [coordinate, victim_color_or_null, null].\n"
|
|
@@ -360,17 +391,34 @@ def _print_actions(actions: list, my_color: str | None = None):
|
|
|
360
391
|
print(f"\n (other players still need to act: {', '.join(sorted(other_colors))})")
|
|
361
392
|
|
|
362
393
|
|
|
394
|
+
def _unpack_record(r):
|
|
395
|
+
"""Unpack an action record into (color, action_type, value).
|
|
396
|
+
|
|
397
|
+
Records may be nested [[color, type, value], result] or flat [color, type, value].
|
|
398
|
+
"""
|
|
399
|
+
if isinstance(r, list) and len(r) >= 2 and isinstance(r[0], list):
|
|
400
|
+
action = r[0]
|
|
401
|
+
color = action[0] if len(action) > 0 else None
|
|
402
|
+
atype = action[1] if len(action) > 1 else None
|
|
403
|
+
val = action[2] if len(action) > 2 else None
|
|
404
|
+
return color, atype, val
|
|
405
|
+
if isinstance(r, list) and len(r) >= 2:
|
|
406
|
+
color = r[0]
|
|
407
|
+
atype = r[1]
|
|
408
|
+
val = r[2] if len(r) > 2 else None
|
|
409
|
+
return color, atype, val
|
|
410
|
+
return None, None, None
|
|
411
|
+
|
|
412
|
+
|
|
363
413
|
def _print_history(records: list, since: int = 0):
|
|
364
414
|
recent = records[since:]
|
|
365
415
|
if not recent:
|
|
366
416
|
return
|
|
367
417
|
_section(f"Recent Actions ({len(recent)} moves)")
|
|
368
418
|
for r in recent:
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
val = r[2] if len(r) > 2 and r[2] is not None else ""
|
|
373
|
-
if val != "":
|
|
419
|
+
color, action, val = _unpack_record(r)
|
|
420
|
+
if color and action:
|
|
421
|
+
if val is not None:
|
|
374
422
|
print(f" {color}: {action} {json.dumps(val, separators=(',', ':'))}")
|
|
375
423
|
else:
|
|
376
424
|
print(f" {color}: {action}")
|
|
@@ -378,6 +426,25 @@ def _print_history(records: list, since: int = 0):
|
|
|
378
426
|
print(f" {r}")
|
|
379
427
|
|
|
380
428
|
|
|
429
|
+
def _count_turns(state: dict) -> int:
|
|
430
|
+
"""Count ROLL_THE_SHELLS records to get the turn number."""
|
|
431
|
+
count = 0
|
|
432
|
+
for r in state.get("action_records", []):
|
|
433
|
+
_, atype, _ = _unpack_record(r)
|
|
434
|
+
if atype == "ROLL_THE_SHELLS":
|
|
435
|
+
count += 1
|
|
436
|
+
return count
|
|
437
|
+
|
|
438
|
+
|
|
439
|
+
def _who_rolled_last(state: dict) -> str | None:
|
|
440
|
+
"""Return the color of the player who made the most recent roll."""
|
|
441
|
+
for r in reversed(state.get("action_records", [])):
|
|
442
|
+
color, atype, _ = _unpack_record(r)
|
|
443
|
+
if atype == "ROLL_THE_SHELLS":
|
|
444
|
+
return color
|
|
445
|
+
return None
|
|
446
|
+
|
|
447
|
+
|
|
381
448
|
def _print_chat(messages: list, label: str = "Chat"):
|
|
382
449
|
if not messages:
|
|
383
450
|
return
|
|
@@ -424,10 +491,10 @@ def _print_join(resp: dict):
|
|
|
424
491
|
print(f" Seat: {resp['seat_index']}")
|
|
425
492
|
print(f" Players: {resp['players_joined']}")
|
|
426
493
|
print(f" Started: {'yes' if resp.get('game_started') else 'no'}")
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
print(f"
|
|
430
|
-
print(f"
|
|
494
|
+
|
|
495
|
+
_save_session(resp["game_id"], resp["token"], resp["player_color"])
|
|
496
|
+
print(f"\n Session saved to {_session_path()}")
|
|
497
|
+
print(f" All subsequent clawtan commands will use this session automatically.")
|
|
431
498
|
|
|
432
499
|
|
|
433
500
|
def cmd_wait(args):
|
|
@@ -510,7 +577,7 @@ def cmd_wait(args):
|
|
|
510
577
|
state = _get(f"/game/{game_id}")
|
|
511
578
|
|
|
512
579
|
prompt = state.get("current_prompt", "?")
|
|
513
|
-
turns =
|
|
580
|
+
turns = status.get("num_turns") or state.get("num_turns") or _count_turns(state)
|
|
514
581
|
|
|
515
582
|
_header("YOUR TURN")
|
|
516
583
|
print(f" Game: {game_id}")
|
|
@@ -551,8 +618,9 @@ def _print_roll_result(state: dict, pre_resources: dict | None):
|
|
|
551
618
|
records = state.get("action_records", [])
|
|
552
619
|
roll_val = None
|
|
553
620
|
for r in reversed(records):
|
|
554
|
-
|
|
555
|
-
|
|
621
|
+
color, atype, val = _unpack_record(r)
|
|
622
|
+
if atype == "ROLL_THE_SHELLS":
|
|
623
|
+
roll_val = val
|
|
556
624
|
break
|
|
557
625
|
|
|
558
626
|
if roll_val is not None:
|
|
@@ -679,29 +747,8 @@ def cmd_act(args):
|
|
|
679
747
|
# (e.g. we also need to discard on a 7)
|
|
680
748
|
print(f" Prompt: {prompt}")
|
|
681
749
|
_print_actions(actions, my_color=color)
|
|
682
|
-
print(
|
|
683
|
-
f"\n Note: {current_color} is also acting (e.g. discarding)."
|
|
684
|
-
f" Your turn will continue after -- run 'clawtan wait'.",
|
|
685
|
-
)
|
|
686
|
-
elif prompt in ("RELEASE_CATCH", "MOVE_THE_KRAKEN", "DISCARD"):
|
|
687
|
-
# Discard/robber phase -- other players are acting but our turn resumes after
|
|
688
|
-
_section("Waiting on Other Players")
|
|
689
|
-
# Figure out which players need to act
|
|
690
|
-
other_colors = set()
|
|
691
|
-
for a in actions:
|
|
692
|
-
if isinstance(a, list) and len(a) > 1 and a[0] and a[0] != color:
|
|
693
|
-
other_colors.add(a[0])
|
|
694
|
-
if other_colors:
|
|
695
|
-
print(f" {', '.join(sorted(other_colors))} must {prompt.lower().replace('_', ' ')} first.")
|
|
696
|
-
else:
|
|
697
|
-
print(f" Current prompt: {prompt} (waiting on {current_color})")
|
|
698
|
-
print(
|
|
699
|
-
f"\n YOUR TURN IS NOT OVER. After they finish, you will continue"
|
|
700
|
-
f" (e.g. move the Kraken, then play your turn)."
|
|
701
|
-
f"\n Run 'clawtan wait' to resume."
|
|
702
|
-
)
|
|
703
750
|
else:
|
|
704
|
-
print(f"\n
|
|
751
|
+
print(f"\n Action done. No more actions available. Run 'clawtan wait' for your next turn or action required.")
|
|
705
752
|
|
|
706
753
|
|
|
707
754
|
def cmd_status(args):
|
|
@@ -968,7 +1015,7 @@ def main():
|
|
|
968
1015
|
" BUY_TREASURE_MAP Buy dev card\n"
|
|
969
1016
|
" SUMMON_LOBSTER_GUARD Play knight card\n"
|
|
970
1017
|
" MOVE_THE_KRAKEN <val> Move robber, e.g. '[[0,1,-1],\"BLUE\",null]'\n"
|
|
971
|
-
" RELEASE_CATCH
|
|
1018
|
+
" RELEASE_CATCH Discard half your cards (server selects randomly)\n"
|
|
972
1019
|
" PLAY_BOUNTIFUL_HARVEST <r> Year of Plenty, e.g. '[\"DRIFTWOOD\",\"CORAL\"]'\n"
|
|
973
1020
|
" PLAY_TIDAL_MONOPOLY <res> Monopoly, e.g. SHRIMP\n"
|
|
974
1021
|
" PLAY_CURRENT_BUILDING Road Building\n"
|