clawtan 0.2.0 → 0.2.1

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 +118 -32
  2. package/package.json +1 -1
package/clawtan/cli.py CHANGED
@@ -14,10 +14,24 @@ Typical agent flow:
14
14
  clawtan act END_TIDE
15
15
  clawtan wait # next turn...
16
16
 
17
+ Multi-player on one machine (each terminal):
18
+ clawtan join <GAME_ID> # join, note your assigned color
19
+ export CLAWTAN_COLOR=<COLOR> # lock this terminal to your player
20
+ clawtan wait # now uses the correct session
21
+
22
+ Or use --player on every call (works with separate exec calls):
23
+ clawtan --player BLUE wait
24
+ clawtan --player BLUE act ROLL_THE_SHELLS
25
+
26
+ Same color in multiple games — add --game:
27
+ clawtan --player RED wait --game <GAME_ID>
28
+
17
29
  Session lookup order (per field):
18
- 1. CLI flags (--game, --token, --color)
30
+ 1. CLI flags (--game, --token, --color, --player)
19
31
  2. Environment variables (CLAWTAN_GAME, CLAWTAN_TOKEN, CLAWTAN_COLOR)
20
- 3. Session file (~/.clawtan_session, override with CLAWTAN_SESSION_FILE)
32
+ 3. Session file: ~/.clawtan_sessions/<game>_<color>.json matched by
33
+ available hints, falling back to ~/.clawtan_session.
34
+ Override with CLAWTAN_SESSION_FILE.
21
35
  """
22
36
 
23
37
  import argparse
@@ -126,42 +140,97 @@ def _get(path, token=None):
126
140
  # ---------------------------------------------------------------------------
127
141
  # Session file helpers
128
142
  # ---------------------------------------------------------------------------
129
- def _session_path() -> str:
130
- return os.environ.get("CLAWTAN_SESSION_FILE") or os.path.expanduser("~/.clawtan_session")
143
+ _SESSIONS_DIR = os.path.expanduser("~/.clawtan_sessions")
131
144
 
132
145
 
133
146
  def _save_session(game_id: str, token: str, color: str):
134
- path = _session_path()
135
147
  data = {"GAME": game_id, "TOKEN": token, "COLOR": color}
148
+ os.makedirs(_SESSIONS_DIR, exist_ok=True)
149
+ path = os.path.join(_SESSIONS_DIR, f"{game_id}_{color}.json")
136
150
  try:
137
151
  with open(path, "w") as f:
138
152
  json.dump(data, f)
139
153
  except OSError as e:
140
154
  print(f"Warning: could not save session to {path}: {e}", file=sys.stderr)
155
+ default = os.environ.get("CLAWTAN_SESSION_FILE") or os.path.expanduser("~/.clawtan_session")
156
+ try:
157
+ with open(default, "w") as f:
158
+ json.dump(data, f)
159
+ except OSError:
160
+ pass
141
161
 
142
162
 
143
- def _load_session() -> dict:
144
- path = _session_path()
163
+ def _find_session(game_hint: str | None = None, color_hint: str | None = None) -> dict:
164
+ """Find a session file using available game/color hints."""
165
+ custom = os.environ.get("CLAWTAN_SESSION_FILE")
166
+ if custom:
167
+ try:
168
+ with open(custom) as f:
169
+ return json.load(f)
170
+ except (OSError, json.JSONDecodeError):
171
+ return {}
172
+
173
+ if game_hint and color_hint:
174
+ path = os.path.join(_SESSIONS_DIR, f"{game_hint}_{color_hint}.json")
175
+ try:
176
+ with open(path) as f:
177
+ return json.load(f)
178
+ except (OSError, json.JSONDecodeError):
179
+ pass
180
+
181
+ if (game_hint or color_hint) and os.path.isdir(_SESSIONS_DIR):
182
+ matches = []
183
+ for fname in os.listdir(_SESSIONS_DIR):
184
+ if not fname.endswith(".json"):
185
+ continue
186
+ try:
187
+ with open(os.path.join(_SESSIONS_DIR, fname)) as f:
188
+ data = json.load(f)
189
+ if game_hint and data.get("GAME") != game_hint:
190
+ continue
191
+ if color_hint and data.get("COLOR") != color_hint:
192
+ continue
193
+ matches.append(data)
194
+ except (OSError, json.JSONDecodeError):
195
+ continue
196
+ if len(matches) == 1:
197
+ return matches[0]
198
+ if matches:
199
+ return matches[0]
200
+
201
+ default = os.path.expanduser("~/.clawtan_session")
145
202
  try:
146
- with open(path) as f:
203
+ with open(default) as f:
147
204
  return json.load(f)
148
205
  except (OSError, json.JSONDecodeError):
149
206
  return {}
150
207
 
151
208
 
152
209
  # ---------------------------------------------------------------------------
153
- # Environment / session variable helpers
210
+ # Session resolution
154
211
  # ---------------------------------------------------------------------------
155
- def _env(name: str, arg_val=None, required=True):
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
212
+ def _resolve_session(arg_game=None, arg_token=None, arg_color=None):
213
+ """Resolve game, token, color from flags -> env vars -> session file.
214
+
215
+ Uses all available hints together to find the correct session file,
216
+ which avoids ambiguity when multiple games or players share a machine.
217
+ """
218
+ game = arg_game or os.environ.get("CLAWTAN_GAME") or None
219
+ token = arg_token or os.environ.get("CLAWTAN_TOKEN") or None
220
+ color = arg_color or os.environ.get("CLAWTAN_COLOR") or None
221
+
222
+ if not (game and token and color):
223
+ session = _find_session(game_hint=game, color_hint=color)
224
+ game = game or session.get("GAME")
225
+ token = token or session.get("TOKEN")
226
+ color = color or session.get("COLOR")
227
+
228
+ return game, token, color
229
+
230
+
231
+ def _require(name: str, val):
232
+ """Exit with error if a required session value is missing."""
162
233
  if not val:
163
- val = _load_session().get(name)
164
- if required and not val:
165
234
  print(
166
235
  f"ERROR: Missing {name}. Pass --{name.lower()}, set CLAWTAN_{name},"
167
236
  f" or run 'clawtan quick-join' to create a session.",
@@ -493,14 +562,20 @@ def _print_join(resp: dict):
493
562
  print(f" Started: {'yes' if resp.get('game_started') else 'no'}")
494
563
 
495
564
  _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.")
565
+ gid = resp["game_id"]
566
+ color = resp["player_color"]
567
+ print(f"\n Session saved to ~/.clawtan_sessions/{gid}_{color}.json")
568
+ print(f"\n Multi-player setup — pick ONE option:")
569
+ print(f" export CLAWTAN_COLOR={color} # env var per terminal")
570
+ print(f" clawtan --player {color} <command> # flag per command")
571
+ print(f" clawtan --player {color} wait --game {gid} # if same color in multiple games")
498
572
 
499
573
 
500
574
  def cmd_wait(args):
501
- game_id = _env("GAME", args.game)
502
- token = _env("TOKEN", args.token)
503
- color = _env("COLOR", args.color)
575
+ game_id, token, color = _resolve_session(args.game, args.token, args.color)
576
+ _require("GAME", game_id)
577
+ _require("TOKEN", token)
578
+ _require("COLOR", color)
504
579
  poll = args.poll
505
580
  deadline = time.monotonic() + args.timeout
506
581
 
@@ -652,9 +727,10 @@ def _print_roll_result(state: dict, pre_resources: dict | None):
652
727
 
653
728
 
654
729
  def cmd_act(args):
655
- game_id = _env("GAME", args.game)
656
- token = _env("TOKEN", args.token)
657
- color = _env("COLOR", args.color)
730
+ game_id, token, color = _resolve_session(args.game, args.token, args.color)
731
+ _require("GAME", game_id)
732
+ _require("TOKEN", token)
733
+ _require("COLOR", color)
658
734
 
659
735
  # Snapshot resources before rolling so we can diff afterwards
660
736
  pre_resources = None
@@ -752,8 +828,8 @@ def cmd_act(args):
752
828
 
753
829
 
754
830
  def cmd_status(args):
755
- game_id = _env("GAME", args.game)
756
- token = _env("TOKEN", args.token, required=False)
831
+ game_id, token, _ = _resolve_session(args.game, args.token)
832
+ _require("GAME", game_id)
757
833
 
758
834
  status = _get(f"/game/{game_id}/status", token=token)
759
835
 
@@ -777,7 +853,8 @@ def cmd_status(args):
777
853
 
778
854
 
779
855
  def cmd_board(args):
780
- game_id = _env("GAME", args.game)
856
+ game_id, _, _ = _resolve_session(args.game)
857
+ _require("GAME", game_id)
781
858
  state = _get(f"/game/{game_id}")
782
859
 
783
860
  if not state.get("started") or not state.get("tiles"):
@@ -907,14 +984,16 @@ def cmd_board(args):
907
984
 
908
985
 
909
986
  def cmd_chat(args):
910
- game_id = _env("GAME", args.game)
911
- token = _env("TOKEN", args.token)
987
+ game_id, token, _ = _resolve_session(args.game, args.token)
988
+ _require("GAME", game_id)
989
+ _require("TOKEN", token)
912
990
  _post(f"/game/{game_id}/chat", {"message": args.message}, token=token)
913
991
  print("Chat sent.")
914
992
 
915
993
 
916
994
  def cmd_chat_read(args):
917
- game_id = _env("GAME", args.game)
995
+ game_id, _, _ = _resolve_session(args.game)
996
+ _require("GAME", game_id)
918
997
  resp = _get(f"/game/{game_id}/chat?since={args.since}")
919
998
  msgs = resp.get("messages", [])
920
999
  if msgs:
@@ -939,6 +1018,11 @@ def main():
939
1018
  ),
940
1019
  formatter_class=argparse.RawDescriptionHelpFormatter,
941
1020
  )
1021
+ parser.add_argument(
1022
+ "--player",
1023
+ metavar="COLOR",
1024
+ help="Player color — selects the per-player session file (for multi-player on one machine)",
1025
+ )
942
1026
  sub = parser.add_subparsers(dest="command", required=True)
943
1027
 
944
1028
  # -- create --------------------------------------------------------
@@ -1088,6 +1172,8 @@ def main():
1088
1172
 
1089
1173
  # -- Parse and run -------------------------------------------------
1090
1174
  args = parser.parse_args()
1175
+ if args.player:
1176
+ os.environ["CLAWTAN_COLOR"] = args.player
1091
1177
  try:
1092
1178
  args.func(args)
1093
1179
  except APIError as e:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clawtan",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
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"