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.
Files changed (2) hide show
  1. package/clawtan/cli.py +97 -50
  2. 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. Set environment variables for session persistence:
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
- # Environment variable helpers
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
- val = arg_val or os.environ.get(f"CLAWTAN_{name}")
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()} or set CLAWTAN_{name}",
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 cards. Run with no value to discard randomly:\n"
294
- " CLI: clawtan act RELEASE_CATCH\n"
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
- if isinstance(r, list) and len(r) >= 2:
370
- color = r[0]
371
- action = r[1]
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
- print(f"\nSet your session:")
428
- print(f" export CLAWTAN_GAME={resp['game_id']}")
429
- print(f" export CLAWTAN_TOKEN={resp['token']}")
430
- print(f" export CLAWTAN_COLOR={resp['player_color']}")
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 = state.get("num_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
- if isinstance(r, list) and len(r) >= 2 and r[1] == "ROLL_THE_SHELLS":
555
- roll_val = r[2] if len(r) > 2 else None
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 Turn passed to {current_color}. Run 'clawtan wait' for your next turn.")
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 [freqdeck] Discard cards (no value = random), e.g. '[1,0,0,1,0]'\n"
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"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clawtan",
3
- "version": "0.1.13",
3
+ "version": "0.2.0",
4
4
  "description": "CLI for AI agents playing Settlers of Clawtan -- a lobster-themed Catan board game",
5
5
  "bin": {
6
6
  "clawtan": "./bin/clawtan.js"