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.
- package/clawtan/cli.py +132 -11
- 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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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"
|