Surn commited on
Commit
f4f5889
·
1 Parent(s): 7b9b5b7

Game update 0.1.5

Browse files

-bug words list, otherwise working

battlewords/__init__.py CHANGED
@@ -1,2 +1,2 @@
1
- __version__ = "0.1.4"
2
  __all__ = ["models", "generator", "logic", "ui"]
 
1
+ __version__ = "0.1.5"
2
  __all__ = ["models", "generator", "logic", "ui"]
battlewords/logic.py CHANGED
@@ -64,7 +64,7 @@ def guess_word(state: GameState, guess_text: str) -> Tuple[bool, int]:
64
  state.guessed.add(target.text)
65
 
66
  state.last_action = f"Correct! +{points} points for {target.text}."
67
- state.can_guess = False
68
  return True, points
69
 
70
 
 
64
  state.guessed.add(target.text)
65
 
66
  state.last_action = f"Correct! +{points} points for {target.text}."
67
+ state.can_guess = True # <-- Allow another guess after a correct guess
68
  return True, points
69
 
70
 
battlewords/ui.py CHANGED
@@ -191,6 +191,22 @@ def inject_styles() -> None:
191
  .shiny-border:hover::before {
192
  left: 100%;
193
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
194
  </style>
195
  """,
196
  unsafe_allow_html=True,
@@ -219,14 +235,14 @@ def _init_session() -> None:
219
  st.session_state.points_by_word = {}
220
  st.session_state.letter_map = build_letter_map(puzzle)
221
  st.session_state.initialized = True
222
-
223
 
224
  def _new_game() -> None:
225
- # Preserve selected wordlist across resets
226
  selected = st.session_state.get("selected_wordlist")
227
  st.session_state.clear()
228
  if selected:
229
  st.session_state.selected_wordlist = selected
 
230
  _init_session()
231
 
232
 
@@ -296,6 +312,7 @@ def _render_sidebar():
296
  _sort_wordlist(st.session_state.selected_wordlist)
297
  else:
298
  st.info("No word lists found in words/ directory. Using built-in fallback.")
 
299
  def get_scope_image(size=4, bgcolor="none", scope_color="green", img_name="scope.gif"):
300
  scope_path = os.path.join(os.path.dirname(__file__), img_name)
301
  if not os.path.exists(scope_path):
@@ -351,36 +368,22 @@ def _render_radar(puzzle: Puzzle, size: int, r_max: float = 0.85, max_frames: in
351
  from matplotlib.animation import FuncAnimation, PillowWriter
352
  from matplotlib.patches import Circle
353
  from matplotlib import colors as mcolors
 
 
354
 
355
- st.markdown(
356
- """
357
- <style>
358
- h3 {
359
- margin: 0.25rem auto;
360
- text-align: center;
361
- }
362
- </style>
363
- """,
364
- unsafe_allow_html=True,
365
- )
366
- st.subheader("Score Board")
367
-
368
- # Radar blip positions
369
  xs = np.array([c.y + 1 for c in puzzle.radar])
370
  ys = np.array([c.x + 1 for c in puzzle.radar])
371
  n_points = len(xs)
372
 
373
- # Animation parameters (in data units)
374
  r_min = 0.15
375
- ring_linewidth = 4 # thickness of the ring stroke
376
 
377
  rgba_labels = mcolors.to_rgba("#FFFFFF", 0.7)
378
  rgba_ticks = mcolors.to_rgba("#FFFFFF", 0.66)
379
  bgcolor="#4b7bc4"
380
  scope_size=3
381
  scope_color="#ffffff"
382
-
383
-
384
  imgscope = get_scope_image(size=scope_size, bgcolor=bgcolor, scope_color=scope_color, img_name="scope_blue.png")
385
  fig, ax = plt.subplots(figsize=(scope_size, scope_size))
386
  ax.set_xlim(0.2, size)
@@ -389,17 +392,13 @@ def _render_radar(puzzle: Puzzle, size: int, r_max: float = 0.85, max_frames: in
389
  ax.set_yticks(range(1, size + 1))
390
  ax.tick_params(axis="both", which="both", labelcolor=rgba_labels)
391
  ax.tick_params(axis="both", which="both", colors=rgba_ticks)
392
- #ax.grid(True, which="both", linestyle="--", alpha=0.3)
393
- # ax.set_title("Radar")
394
  ax.set_aspect('equal', adjustable='box')
395
 
396
- # Build a linear gradient background on the figure (outside the main axes)
397
  def _make_linear_gradient(width: int, height: int, angle_deg: float,
398
  colors_hex: list[str], stops: list[float]) -> np.ndarray:
399
  yy, xx = np.meshgrid(np.linspace(0, 1, height), np.linspace(0, 1, width), indexing='ij')
400
  theta = np.deg2rad(angle_deg)
401
  proj = np.cos(theta) * xx + np.sin(theta) * yy
402
- # Normalize projection to [0,1] using corner extrema
403
  corners = np.array([[0, 0], [1, 0], [0, 1], [1, 1]], dtype=float)
404
  pc = np.cos(theta) * corners[:, 0] + np.sin(theta) * corners[:, 1]
405
  proj = (proj - pc.min()) / (pc.max() - pc.min() + 1e-12)
@@ -408,7 +407,6 @@ def _render_radar(puzzle: Puzzle, size: int, r_max: float = 0.85, max_frames: in
408
  stop_arr = np.asarray(stops, dtype=float)
409
  cols = np.asarray([mcolors.to_rgb(c) for c in colors_hex], dtype=float)
410
 
411
- # For each pixel pick the interval and interpolate colors
412
  j = np.clip(np.searchsorted(stop_arr, proj, side='right') - 1, 0, len(stop_arr) - 2)
413
  a = stop_arr[j]
414
  b = stop_arr[j + 1]
@@ -418,7 +416,6 @@ def _render_radar(puzzle: Puzzle, size: int, r_max: float = 0.85, max_frames: in
418
  img = (1.0 - w) * c0 + w * c1
419
  return img
420
 
421
- # Size gradient to the rendered figure pixel size
422
  fig_w, fig_h = [int(v) for v in fig.canvas.get_width_height()]
423
  grad_img = _make_linear_gradient(
424
  width=fig_w,
@@ -427,27 +424,19 @@ def _render_radar(puzzle: Puzzle, size: int, r_max: float = 0.85, max_frames: in
427
  colors_hex=['#a1a1a1', '#ffffff', '#a1a1a1', '#666666'],
428
  stops=[0.0, 1.0 / 3.0, 2.0 / 3.0, 1.0],
429
  )
430
- # Background axes that spans the whole figure (zorder below main axes)
431
  bg_ax = fig.add_axes([0, 0, 1, 1], zorder=0)
432
  bg_ax.imshow(grad_img, aspect='auto', interpolation='bilinear')
433
  bg_ax.axis('off')
434
 
435
- # Overlay the radar scope image centered in the main axes
436
  scope_ax = fig.add_axes([-0.075, -0.075, 1.15, 1.15], zorder=1)
437
  scope_ax.imshow(imgscope, aspect='auto', interpolation='lanczos')
438
  scope_ax.axis('off')
439
 
440
- # Main axes above the background, with solid interior color for the radar grid
441
- ax.set_facecolor('none') # interior of the radar
442
  ax.set_zorder(2)
443
- #ax.axis('off')
444
- # Hide decorations but keep patch/frame on
445
  for spine in ax.spines.values():
446
  spine.set_visible(False)
447
- # ax.set_xticks([])
448
- # ax.set_yticks([])
449
 
450
- # Create ring (annulus-like) patches for each radar blip using Circle with stroke
451
  rings: list[Circle] = []
452
  for x, y in zip(xs, ys):
453
  ring = Circle((x, y), radius=r_min, fill=False, edgecolor='#9ceffe', linewidth=ring_linewidth, alpha=1.0, zorder=3)
@@ -473,18 +462,24 @@ def _render_radar(puzzle: Puzzle, size: int, r_max: float = 0.85, max_frames: in
473
  ring.set_alpha(alpha_i)
474
  return rings
475
 
476
- ani = FuncAnimation(fig, update, frames=max_frames, interval=50, blit=True)
 
 
 
 
 
 
 
477
 
478
- # Save animation to a temporary GIF file
479
  with tempfile.NamedTemporaryFile(suffix=".gif", delete=False) as tmpfile:
 
480
  ani.save(tmpfile.name, writer=PillowWriter(fps=20))
481
  plt.close(fig)
482
- # Read the GIF into memory
483
  tmpfile.seek(0)
484
  gif_bytes = tmpfile.read()
 
485
  st.image(gif_bytes, width='content', output_format="auto")
486
- os.unlink(tmpfile.name)
487
-
488
 
489
  def _render_grid(state: GameState, letter_map):
490
  size = state.grid_size
@@ -576,6 +571,34 @@ def _render_grid(state: GameState, letter_map):
576
  _sync_back(state)
577
 
578
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
579
  def _render_guess_form(state: GameState):
580
  with st.form("guess_form"):
581
  guess_text = st.text_input("Your guess", value="", max_chars=12)
@@ -652,10 +675,15 @@ def run_app():
652
  left, right = st.columns([2, 2], gap="medium")
653
  with left:
654
  _render_grid(state, st.session_state.letter_map)
655
- st.divider()
656
- _render_guess_form(state)
657
  with right:
658
- _render_radar(state.puzzle, size=state.grid_size, r_max=1.6, max_frames=60, sinusoid_expand=False, stagger_radar=True)
 
 
 
 
 
 
659
  _render_score_panel(state)
660
 
661
 
 
191
  .shiny-border:hover::before {
192
  left: 100%;
193
  }
194
+
195
+ /* Hit/Miss radio indicators - circular group */
196
+ .bw-radio-group { display:flex; align-items:center; gap: 10px; flex-flow: column;}
197
+ .bw-radio-item { display:flex; flex-direction:column; align-items:center; gap:6px; }
198
+ .bw-radio-circle {
199
+ width: 46px; height: 46px; border-radius: 50%;
200
+ border: 4px solid; /* border-image: linear-gradient(45deg, #a1a1a1, #ffffff, #a1a1a1, #666666) 1; */
201
+ background: rgba(255,255,255,0.06);
202
+ display: grid; place-items: center; color:#fff; font-weight:700;
203
+ }
204
+ .bw-radio-circle .dot { width: 14px; height: 14px; border-radius: 50%; background:#777; box-shadow: inset 0 0 0 2px rgba(255,255,255,0.25); }
205
+ .bw-radio-circle.active.hit { background: linear-gradient(135deg, rgba(0,255,127,0.18), rgba(0,128,64,0.38)); }
206
+ .bw-radio-circle.active.hit .dot { background:#20d46c; box-shadow: 0 0 10px rgba(32,212,108,0.85); }
207
+ .bw-radio-circle.active.miss { background: linear-gradient(135deg, rgba(255,0,0,0.18), rgba(128,0,0,0.38)); }
208
+ .bw-radio-circle.active.miss .dot { background:#ff4b4b; box-shadow: 0 0 10px rgba(255,75,75,0.85); }
209
+ .bw-radio-caption { font-size: 0.8rem; color:#fff; opacity:0.85; letter-spacing:0.5px; }
210
  </style>
211
  """,
212
  unsafe_allow_html=True,
 
235
  st.session_state.points_by_word = {}
236
  st.session_state.letter_map = build_letter_map(puzzle)
237
  st.session_state.initialized = True
238
+ st.session_state.radar_gif_path = None # Add this line
239
 
240
  def _new_game() -> None:
 
241
  selected = st.session_state.get("selected_wordlist")
242
  st.session_state.clear()
243
  if selected:
244
  st.session_state.selected_wordlist = selected
245
+ st.session_state.radar_gif_path = None # Reset radar GIF path
246
  _init_session()
247
 
248
 
 
312
  _sort_wordlist(st.session_state.selected_wordlist)
313
  else:
314
  st.info("No word lists found in words/ directory. Using built-in fallback.")
315
+
316
  def get_scope_image(size=4, bgcolor="none", scope_color="green", img_name="scope.gif"):
317
  scope_path = os.path.join(os.path.dirname(__file__), img_name)
318
  if not os.path.exists(scope_path):
 
368
  from matplotlib.animation import FuncAnimation, PillowWriter
369
  from matplotlib.patches import Circle
370
  from matplotlib import colors as mcolors
371
+ import tempfile
372
+ import os
373
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
374
  xs = np.array([c.y + 1 for c in puzzle.radar])
375
  ys = np.array([c.x + 1 for c in puzzle.radar])
376
  n_points = len(xs)
377
 
 
378
  r_min = 0.15
379
+ ring_linewidth = 4
380
 
381
  rgba_labels = mcolors.to_rgba("#FFFFFF", 0.7)
382
  rgba_ticks = mcolors.to_rgba("#FFFFFF", 0.66)
383
  bgcolor="#4b7bc4"
384
  scope_size=3
385
  scope_color="#ffffff"
386
+
 
387
  imgscope = get_scope_image(size=scope_size, bgcolor=bgcolor, scope_color=scope_color, img_name="scope_blue.png")
388
  fig, ax = plt.subplots(figsize=(scope_size, scope_size))
389
  ax.set_xlim(0.2, size)
 
392
  ax.set_yticks(range(1, size + 1))
393
  ax.tick_params(axis="both", which="both", labelcolor=rgba_labels)
394
  ax.tick_params(axis="both", which="both", colors=rgba_ticks)
 
 
395
  ax.set_aspect('equal', adjustable='box')
396
 
 
397
  def _make_linear_gradient(width: int, height: int, angle_deg: float,
398
  colors_hex: list[str], stops: list[float]) -> np.ndarray:
399
  yy, xx = np.meshgrid(np.linspace(0, 1, height), np.linspace(0, 1, width), indexing='ij')
400
  theta = np.deg2rad(angle_deg)
401
  proj = np.cos(theta) * xx + np.sin(theta) * yy
 
402
  corners = np.array([[0, 0], [1, 0], [0, 1], [1, 1]], dtype=float)
403
  pc = np.cos(theta) * corners[:, 0] + np.sin(theta) * corners[:, 1]
404
  proj = (proj - pc.min()) / (pc.max() - pc.min() + 1e-12)
 
407
  stop_arr = np.asarray(stops, dtype=float)
408
  cols = np.asarray([mcolors.to_rgb(c) for c in colors_hex], dtype=float)
409
 
 
410
  j = np.clip(np.searchsorted(stop_arr, proj, side='right') - 1, 0, len(stop_arr) - 2)
411
  a = stop_arr[j]
412
  b = stop_arr[j + 1]
 
416
  img = (1.0 - w) * c0 + w * c1
417
  return img
418
 
 
419
  fig_w, fig_h = [int(v) for v in fig.canvas.get_width_height()]
420
  grad_img = _make_linear_gradient(
421
  width=fig_w,
 
424
  colors_hex=['#a1a1a1', '#ffffff', '#a1a1a1', '#666666'],
425
  stops=[0.0, 1.0 / 3.0, 2.0 / 3.0, 1.0],
426
  )
 
427
  bg_ax = fig.add_axes([0, 0, 1, 1], zorder=0)
428
  bg_ax.imshow(grad_img, aspect='auto', interpolation='bilinear')
429
  bg_ax.axis('off')
430
 
 
431
  scope_ax = fig.add_axes([-0.075, -0.075, 1.15, 1.15], zorder=1)
432
  scope_ax.imshow(imgscope, aspect='auto', interpolation='lanczos')
433
  scope_ax.axis('off')
434
 
435
+ ax.set_facecolor('none')
 
436
  ax.set_zorder(2)
 
 
437
  for spine in ax.spines.values():
438
  spine.set_visible(False)
 
 
439
 
 
440
  rings: list[Circle] = []
441
  for x, y in zip(xs, ys):
442
  ring = Circle((x, y), radius=r_min, fill=False, edgecolor='#9ceffe', linewidth=ring_linewidth, alpha=1.0, zorder=3)
 
462
  ring.set_alpha(alpha_i)
463
  return rings
464
 
465
+ # Use persistent GIF if available
466
+ gif_path = st.session_state.get("radar_gif_path")
467
+ if gif_path and os.path.exists(gif_path):
468
+ with open(gif_path, "rb") as f:
469
+ gif_bytes = f.read()
470
+ st.image(gif_bytes, width='content', output_format="auto")
471
+ plt.close(fig)
472
+ return
473
 
474
+ # Otherwise, generate and persist
475
  with tempfile.NamedTemporaryFile(suffix=".gif", delete=False) as tmpfile:
476
+ ani = FuncAnimation(fig, update, frames=max_frames, interval=50, blit=True)
477
  ani.save(tmpfile.name, writer=PillowWriter(fps=20))
478
  plt.close(fig)
 
479
  tmpfile.seek(0)
480
  gif_bytes = tmpfile.read()
481
+ st.session_state.radar_gif_path = tmpfile.name # Save path for reuse
482
  st.image(gif_bytes, width='content', output_format="auto")
 
 
483
 
484
  def _render_grid(state: GameState, letter_map):
485
  size = state.grid_size
 
571
  _sync_back(state)
572
 
573
 
574
+ def _render_hit_miss(state: GameState):
575
+ # Determine last reveal outcome from last_action string
576
+ action = (state.last_action or "").strip()
577
+ is_hit = action.startswith("Revealed '")
578
+ is_miss = action.startswith("Revealed empty")
579
+
580
+ # Render as a circular radio group, side-by-side
581
+ st.markdown(
582
+ f"""
583
+ <div class="bw-radio-group" role="radiogroup" aria-label="Hit or Miss">
584
+ <div class="bw-radio-item">
585
+ <div class="bw-radio-circle {'active hit' if is_hit else ''}" role="radio" aria-checked="{'true' if is_hit else 'false'}" aria-label="Hit">
586
+ <span class="dot"></span>
587
+ </div>
588
+ <div class="bw-radio-caption">HIT</div>
589
+ </div>
590
+ <div class="bw-radio-item">
591
+ <div class="bw-radio-circle {'active miss' if is_miss else ''}" role="radio" aria-checked="{'true' if is_miss else 'false'}" aria-label="Miss">
592
+ <span class="dot"></span>
593
+ </div>
594
+ <div class="bw-radio-caption">MISS</div>
595
+ </div>
596
+ </div>
597
+ """,
598
+ unsafe_allow_html=True,
599
+ )
600
+
601
+
602
  def _render_guess_form(state: GameState):
603
  with st.form("guess_form"):
604
  guess_text = st.text_input("Your guess", value="", max_chars=12)
 
675
  left, right = st.columns([2, 2], gap="medium")
676
  with left:
677
  _render_grid(state, st.session_state.letter_map)
678
+
 
679
  with right:
680
+ one, two = st.columns([1, 5], gap="small")
681
+ with one:
682
+ _render_hit_miss(state)
683
+ with two:
684
+ _render_radar(state.puzzle, size=state.grid_size, r_max=1.6, max_frames=60, sinusoid_expand=False, stagger_radar=True)
685
+ #st.divider()
686
+ _render_guess_form(state)
687
  _render_score_panel(state)
688
 
689