clawtan 0.1.6 → 0.1.10

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 +132 -11
  2. package/package.json +1 -1
package/clawtan/cli.py CHANGED
@@ -191,6 +191,17 @@ def _my_status(state: dict, color: str) -> dict | None:
191
191
  }
192
192
 
193
193
 
194
+ def _all_player_resources(state: dict) -> dict:
195
+ """Return {color: {resource: count}} for every player."""
196
+ ps = state.get("player_state", {})
197
+ colors = state.get("colors", [])
198
+ result = {}
199
+ for i, c in enumerate(colors):
200
+ p = f"P{i}_"
201
+ result[c] = {r: ps.get(f"{p}{r}_IN_HAND", 0) for r in RESOURCES}
202
+ return result
203
+
204
+
194
205
  def _opponents(state: dict, color: str) -> list:
195
206
  ps = state.get("player_state", {})
196
207
  colors = state.get("colors", [])
@@ -220,9 +231,9 @@ def _opponents(state: dict, color: str) -> list:
220
231
  return result
221
232
 
222
233
 
223
- # ---------------------------------------------------------------------------
234
+ # --------------------------------------------------------------------------
224
235
  # Text formatters
225
- # ---------------------------------------------------------------------------
236
+ # --------------------------------------------------------------------------
226
237
  def _header(title: str):
227
238
  print(f"\n=== {title} ===")
228
239
 
@@ -277,26 +288,76 @@ def _print_opponents(opponents: list):
277
288
  print(line)
278
289
 
279
290
 
280
- def _print_actions(actions: list):
291
+ _ACTION_HINTS = {
292
+ "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]'"
297
+ ),
298
+ "MOVE_THE_KRAKEN": (
299
+ "Move robber: value = [coordinate, victim_color_or_null, null].\n"
300
+ " CLI: clawtan act MOVE_THE_KRAKEN '[[0,1,-1],\"BLUE\",null]'"
301
+ ),
302
+ "OCEAN_TRADE": (
303
+ "Maritime trade: give 4 (or 3/2 with port) of one resource, receive 1.\n"
304
+ " Value = list of resources: first N are given, last 1 is received.\n"
305
+ " CLI: clawtan act OCEAN_TRADE '[\"KELP\",\"KELP\",\"KELP\",\"KELP\",\"SHRIMP\"]'"
306
+ ),
307
+ "PLAY_BOUNTIFUL_HARVEST": (
308
+ "Year of Plenty: pick 2 free resources.\n"
309
+ " CLI: clawtan act PLAY_BOUNTIFUL_HARVEST '[\"DRIFTWOOD\",\"CORAL\"]'"
310
+ ),
311
+ }
312
+
313
+
314
+ def _print_actions(actions: list, my_color: str | None = None):
281
315
  _section("Available Actions")
282
- grouped = defaultdict(list)
316
+
317
+ my_actions = []
318
+ other_colors = set()
283
319
  for a in actions:
320
+ if isinstance(a, list) and len(a) > 1:
321
+ action_color = a[0]
322
+ if my_color and action_color and action_color != my_color:
323
+ other_colors.add(action_color)
324
+ continue
325
+ my_actions.append(a)
326
+
327
+ if not my_actions and other_colors:
328
+ print(f" (none for you -- waiting on: {', '.join(sorted(other_colors))})")
329
+ return
330
+
331
+ grouped = defaultdict(list)
332
+ for a in my_actions:
284
333
  atype = a[1] if isinstance(a, list) and len(a) > 1 else str(a)
285
334
  val = a[2] if isinstance(a, list) and len(a) > 2 else None
286
335
  grouped[atype].append(val)
287
336
 
288
337
  for atype, values in grouped.items():
338
+ hint = _ACTION_HINTS.get(atype)
289
339
  if all(v is None for v in values):
290
340
  print(f" {atype}")
341
+ if hint:
342
+ print(f" ({hint})")
291
343
  else:
292
344
  formatted = [json.dumps(v, separators=(",", ":")) for v in values]
293
- joined = " | ".join(formatted)
294
- if len(joined) + len(atype) + 4 <= 120:
295
- print(f" {atype}: {joined}")
296
- else:
345
+ if hint:
297
346
  print(f" {atype} ({len(values)} options):")
347
+ print(f" ({hint})")
298
348
  for f in formatted:
299
349
  print(f" {f}")
350
+ else:
351
+ joined = " | ".join(formatted)
352
+ if len(joined) + len(atype) + 4 <= 120:
353
+ print(f" {atype}: {joined}")
354
+ else:
355
+ print(f" {atype} ({len(values)} options):")
356
+ for f in formatted:
357
+ print(f" {f}")
358
+
359
+ if other_colors:
360
+ print(f"\n (other players still need to act: {', '.join(sorted(other_colors))})")
300
361
 
301
362
 
302
363
  def _print_history(records: list, since: int = 0):
@@ -477,18 +538,65 @@ def cmd_wait(args):
477
538
 
478
539
  actions = state.get("current_playable_actions", [])
479
540
  if actions:
480
- _print_actions(actions)
541
+ _print_actions(actions, my_color=color)
481
542
 
482
543
  robber = state.get("robber_coordinate")
483
544
  if robber:
484
545
  print(f"\n Robber: {robber}")
485
546
 
486
547
 
548
+ def _print_roll_result(state: dict, pre_resources: dict | None):
549
+ """Show dice result and per-player resource gains after a roll."""
550
+ # Extract the roll value from the last action record
551
+ records = state.get("action_records", [])
552
+ roll_val = None
553
+ 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
556
+ break
557
+
558
+ if roll_val is not None:
559
+ if isinstance(roll_val, list) and len(roll_val) == 2:
560
+ print(f" Rolled: {roll_val[0]} + {roll_val[1]} = {sum(roll_val)}")
561
+ else:
562
+ print(f" Rolled: {roll_val}")
563
+
564
+ # Diff resources to show what was distributed
565
+ if pre_resources:
566
+ post_resources = _all_player_resources(state)
567
+ _section("Resources Distributed")
568
+ any_gains = False
569
+ for c in state.get("colors", []):
570
+ pre = pre_resources.get(c, {})
571
+ post = post_resources.get(c, {})
572
+ gains = []
573
+ for res in RESOURCES:
574
+ diff = post.get(res, 0) - pre.get(res, 0)
575
+ if diff > 0:
576
+ gains.append(f"+{diff} {res}")
577
+ elif diff < 0:
578
+ gains.append(f"{diff} {res}")
579
+ if gains:
580
+ any_gains = True
581
+ print(f" {c}: {', '.join(gains)}")
582
+ if not any_gains:
583
+ print(" No resources produced.")
584
+
585
+
487
586
  def cmd_act(args):
488
587
  game_id = _env("GAME", args.game)
489
588
  token = _env("TOKEN", args.token)
490
589
  color = _env("COLOR", args.color)
491
590
 
591
+ # Snapshot resources before rolling so we can diff afterwards
592
+ pre_resources = None
593
+ if args.action == "ROLL_THE_SHELLS":
594
+ try:
595
+ pre_state = _get(f"/game/{game_id}")
596
+ pre_resources = _all_player_resources(pre_state)
597
+ except (APIError, Exception):
598
+ pass
599
+
492
600
  # Parse value: try JSON, fall back to bare string
493
601
  value = None
494
602
  if args.value is not None:
@@ -511,6 +619,10 @@ def cmd_act(args):
511
619
  state = _get(f"/game/{game_id}")
512
620
  current_color = state.get("current_color")
513
621
 
622
+ # After a roll, show the dice result and resource distribution
623
+ if args.action == "ROLL_THE_SHELLS":
624
+ _print_roll_result(state, pre_resources)
625
+
514
626
  if current_color == color:
515
627
  prompt = state.get("current_prompt", "?")
516
628
  print(f" Prompt: {prompt}")
@@ -522,7 +634,7 @@ def cmd_act(args):
522
634
 
523
635
  actions = state.get("current_playable_actions", [])
524
636
  if actions:
525
- _print_actions(actions)
637
+ _print_actions(actions, my_color=color)
526
638
  else:
527
639
  print("\n No actions available.")
528
640
  else:
@@ -558,6 +670,15 @@ def cmd_board(args):
558
670
  game_id = _env("GAME", args.game)
559
671
  state = _get(f"/game/{game_id}")
560
672
 
673
+ if not state.get("started") or not state.get("tiles"):
674
+ _header("BOARD")
675
+ pj = len(state.get("colors", []))
676
+ np = state.get("num_players", "?")
677
+ print(f" The board is not available yet -- the game has not started.")
678
+ print(f" Players joined: {pj}/{np}")
679
+ print(f"\n Use 'clawtan wait' to block until the game starts and it's your turn.")
680
+ return
681
+
561
682
  _header("BOARD")
562
683
 
563
684
  # Tiles and ports
@@ -739,7 +860,7 @@ def main():
739
860
  " BUY_TREASURE_MAP Buy dev card\n"
740
861
  " SUMMON_LOBSTER_GUARD Play knight card\n"
741
862
  " MOVE_THE_KRAKEN <val> Move robber, e.g. '[[0,1,-1],\"BLUE\",null]'\n"
742
- " RELEASE_CATCH <freqdeck> Discard cards, e.g. '[1,0,0,1,0]'\n"
863
+ " RELEASE_CATCH [freqdeck] Discard cards (no value = random), e.g. '[1,0,0,1,0]'\n"
743
864
  " PLAY_BOUNTIFUL_HARVEST <r> Year of Plenty, e.g. '[\"DRIFTWOOD\",\"CORAL\"]'\n"
744
865
  " PLAY_TIDAL_MONOPOLY <res> Monopoly, e.g. SHRIMP\n"
745
866
  " PLAY_CURRENT_BUILDING Road Building\n"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clawtan",
3
- "version": "0.1.6",
3
+ "version": "0.1.10",
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"