Spaces:
Running
Running
Radar scope update 1
Browse files- battlewords/__init__.py +1 -1
- battlewords/ui.py +73 -6
battlewords/__init__.py
CHANGED
|
@@ -1,2 +1,2 @@
|
|
| 1 |
-
__version__ = "0.1.
|
| 2 |
__all__ = ["models", "generator", "logic", "ui"]
|
|
|
|
| 1 |
+
__version__ = "0.1.4"
|
| 2 |
__all__ = ["models", "generator", "logic", "ui"]
|
battlewords/ui.py
CHANGED
|
@@ -3,8 +3,11 @@ from . import __version__ as version
|
|
| 3 |
from typing import Iterable, Tuple, Optional
|
| 4 |
|
| 5 |
import matplotlib.pyplot as plt
|
|
|
|
|
|
|
| 6 |
import tempfile
|
| 7 |
import os
|
|
|
|
| 8 |
|
| 9 |
from .generator import generate_puzzle, sort_word_file
|
| 10 |
from .logic import build_letter_map, reveal_cell, guess_word, is_game_over, compute_tier
|
|
@@ -14,6 +17,12 @@ from .word_loader import get_wordlist_files, load_word_list # use loader direct
|
|
| 14 |
|
| 15 |
CoordLike = Tuple[int, int]
|
| 16 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 17 |
|
| 18 |
def _coord_to_xy(c) -> CoordLike:
|
| 19 |
# Supports dataclass Coord(x, y) or a 2-tuple/list.
|
|
@@ -288,6 +297,45 @@ def _render_sidebar():
|
|
| 288 |
else:
|
| 289 |
st.info("No word lists found in words/ directory. Using built-in fallback.")
|
| 290 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 291 |
|
| 292 |
def _render_radar(puzzle: Puzzle, size: int, r_max: float = 0.85, max_frames: int = 30, sinusoid_expand: bool = True, stagger_radar: bool = False):
|
| 293 |
import numpy as np
|
|
@@ -318,11 +366,19 @@ def _render_radar(puzzle: Puzzle, size: int, r_max: float = 0.85, max_frames: in
|
|
| 318 |
r_min = 0.15
|
| 319 |
ring_linewidth = 4 # thickness of the ring stroke
|
| 320 |
|
| 321 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 322 |
ax.set_xlim(0.2, size)
|
| 323 |
ax.set_ylim(size, 0.2)
|
| 324 |
ax.set_xticks(range(1, size + 1))
|
| 325 |
ax.set_yticks(range(1, size + 1))
|
|
|
|
|
|
|
| 326 |
ax.grid(True, which="both", linestyle="--", alpha=0.3)
|
| 327 |
# ax.set_title("Radar")
|
| 328 |
ax.set_aspect('equal', adjustable='box')
|
|
@@ -366,14 +422,25 @@ def _render_radar(puzzle: Puzzle, size: int, r_max: float = 0.85, max_frames: in
|
|
| 366 |
bg_ax.imshow(grad_img, aspect='auto', interpolation='bilinear')
|
| 367 |
bg_ax.axis('off')
|
| 368 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 369 |
# Main axes above the background, with solid interior color for the radar grid
|
| 370 |
-
ax.set_facecolor('
|
| 371 |
-
ax.set_zorder(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 372 |
|
| 373 |
# Create ring (annulus-like) patches for each radar blip using Circle with stroke
|
| 374 |
rings: list[Circle] = []
|
| 375 |
for x, y in zip(xs, ys):
|
| 376 |
-
ring = Circle((x, y), radius=r_min, fill=False, edgecolor='#9ceffe', linewidth=ring_linewidth, alpha=1.0, zorder=
|
| 377 |
ax.add_patch(ring)
|
| 378 |
rings.append(ring)
|
| 379 |
|
|
@@ -405,8 +472,8 @@ def _render_radar(puzzle: Puzzle, size: int, r_max: float = 0.85, max_frames: in
|
|
| 405 |
# Read the GIF into memory
|
| 406 |
tmpfile.seek(0)
|
| 407 |
gif_bytes = tmpfile.read()
|
| 408 |
-
st.image(gif_bytes, width='
|
| 409 |
-
os.unlink(tmpfile.name)
|
| 410 |
|
| 411 |
|
| 412 |
def _render_grid(state: GameState, letter_map):
|
|
|
|
| 3 |
from typing import Iterable, Tuple, Optional
|
| 4 |
|
| 5 |
import matplotlib.pyplot as plt
|
| 6 |
+
from matplotlib.backends.backend_agg import FigureCanvasAgg as FigureCanvas
|
| 7 |
+
from matplotlib import colors as mcolors
|
| 8 |
import tempfile
|
| 9 |
import os
|
| 10 |
+
from PIL import Image
|
| 11 |
|
| 12 |
from .generator import generate_puzzle, sort_word_file
|
| 13 |
from .logic import build_letter_map, reveal_cell, guess_word, is_game_over, compute_tier
|
|
|
|
| 17 |
|
| 18 |
CoordLike = Tuple[int, int]
|
| 19 |
|
| 20 |
+
def fig_to_pil_rgba(fig):
|
| 21 |
+
canvas = FigureCanvas(fig)
|
| 22 |
+
canvas.draw()
|
| 23 |
+
w, h = fig.canvas.get_width_height()
|
| 24 |
+
img = np.frombuffer(canvas.buffer_rgba(), dtype=np.uint8).reshape(h, w, 4)
|
| 25 |
+
return Image.fromarray(img, mode="RGBA")
|
| 26 |
|
| 27 |
def _coord_to_xy(c) -> CoordLike:
|
| 28 |
# Supports dataclass Coord(x, y) or a 2-tuple/list.
|
|
|
|
| 297 |
else:
|
| 298 |
st.info("No word lists found in words/ directory. Using built-in fallback.")
|
| 299 |
|
| 300 |
+
def _create_radar_scope(size=4, bgcolor="none", scope_color="green"):
|
| 301 |
+
fig, ax = plt.subplots(figsize=(size, size), dpi=100)
|
| 302 |
+
ax.set_facecolor(bgcolor)
|
| 303 |
+
fig.patch.set_alpha(0.5)
|
| 304 |
+
ax.set_zorder(0)
|
| 305 |
+
|
| 306 |
+
|
| 307 |
+
# Hide decorations but keep patch/frame on
|
| 308 |
+
for spine in ax.spines.values():
|
| 309 |
+
spine.set_visible(False)
|
| 310 |
+
ax.set_xticks([])
|
| 311 |
+
ax.set_yticks([])
|
| 312 |
+
|
| 313 |
+
# Center lines
|
| 314 |
+
ax.axhline(0, color=scope_color, alpha=0.8, zorder=1)
|
| 315 |
+
ax.axvline(0, color=scope_color, alpha=0.8, zorder=1)
|
| 316 |
+
# ax.set_xticks(range(1, size + 1))
|
| 317 |
+
# ax.set_yticks(range(1, size + 1))
|
| 318 |
+
|
| 319 |
+
# Circles at 25% and 50% radius
|
| 320 |
+
for radius in [0.33, 0.66, 1.0]:
|
| 321 |
+
circle = plt.Circle((0, 0), radius, fill=False, color=scope_color, alpha=0.8, zorder=1)
|
| 322 |
+
ax.add_patch(circle)
|
| 323 |
+
|
| 324 |
+
# Radial lines at 0, 30, 45, 90 degrees
|
| 325 |
+
angles = [0, 30, 60, 120, 150, 210, 240, 300, 330]
|
| 326 |
+
for angle in angles:
|
| 327 |
+
rad = np.deg2rad(angle)
|
| 328 |
+
x = np.cos(rad)
|
| 329 |
+
y = np.sin(rad)
|
| 330 |
+
ax.plot([0, x], [0, y], color=scope_color, alpha=0.5, zorder=1)
|
| 331 |
+
|
| 332 |
+
# Set limits and remove axes
|
| 333 |
+
#ax.set_xlim(-0.5, 0.5)
|
| 334 |
+
#ax.set_ylim(-0.5, 0.5)
|
| 335 |
+
ax.set_aspect('equal', adjustable='box')
|
| 336 |
+
#ax.axis('off')
|
| 337 |
+
|
| 338 |
+
return fig, ax
|
| 339 |
|
| 340 |
def _render_radar(puzzle: Puzzle, size: int, r_max: float = 0.85, max_frames: int = 30, sinusoid_expand: bool = True, stagger_radar: bool = False):
|
| 341 |
import numpy as np
|
|
|
|
| 366 |
r_min = 0.15
|
| 367 |
ring_linewidth = 4 # thickness of the ring stroke
|
| 368 |
|
| 369 |
+
rgba_labels = mcolors.to_rgba("#FFFFFF", 0.7)
|
| 370 |
+
rgba_ticks = mcolors.to_rgba("#FFFFFF", 0.66)
|
| 371 |
+
|
| 372 |
+
fig, ax = _create_radar_scope(size=3, bgcolor="#4b7bc4", scope_color="#ffffff")
|
| 373 |
+
imgscope = fig_to_pil_rgba(fig)
|
| 374 |
+
plt.close(fig)
|
| 375 |
+
fig, ax = plt.subplots(figsize=(3, 3))
|
| 376 |
ax.set_xlim(0.2, size)
|
| 377 |
ax.set_ylim(size, 0.2)
|
| 378 |
ax.set_xticks(range(1, size + 1))
|
| 379 |
ax.set_yticks(range(1, size + 1))
|
| 380 |
+
ax.tick_params(axis="both", which="both", labelcolor=rgba_labels)
|
| 381 |
+
ax.tick_params(axis="both", which="both", colors=rgba_ticks)
|
| 382 |
ax.grid(True, which="both", linestyle="--", alpha=0.3)
|
| 383 |
# ax.set_title("Radar")
|
| 384 |
ax.set_aspect('equal', adjustable='box')
|
|
|
|
| 422 |
bg_ax.imshow(grad_img, aspect='auto', interpolation='bilinear')
|
| 423 |
bg_ax.axis('off')
|
| 424 |
|
| 425 |
+
# Overlay the radar scope image centered in the main axes
|
| 426 |
+
scope_ax = fig.add_axes([-0.075, -0.075, 1.15, 1.15], zorder=1)
|
| 427 |
+
scope_ax.imshow(imgscope, aspect='auto', interpolation='lanczos')
|
| 428 |
+
scope_ax.axis('off')
|
| 429 |
+
|
| 430 |
# Main axes above the background, with solid interior color for the radar grid
|
| 431 |
+
ax.set_facecolor('none') # interior of the radar
|
| 432 |
+
ax.set_zorder(2)
|
| 433 |
+
#ax.axis('off')
|
| 434 |
+
# Hide decorations but keep patch/frame on
|
| 435 |
+
for spine in ax.spines.values():
|
| 436 |
+
spine.set_visible(False)
|
| 437 |
+
# ax.set_xticks([])
|
| 438 |
+
# ax.set_yticks([])
|
| 439 |
|
| 440 |
# Create ring (annulus-like) patches for each radar blip using Circle with stroke
|
| 441 |
rings: list[Circle] = []
|
| 442 |
for x, y in zip(xs, ys):
|
| 443 |
+
ring = Circle((x, y), radius=r_min, fill=False, edgecolor='#9ceffe', linewidth=ring_linewidth, alpha=1.0, zorder=3)
|
| 444 |
ax.add_patch(ring)
|
| 445 |
rings.append(ring)
|
| 446 |
|
|
|
|
| 472 |
# Read the GIF into memory
|
| 473 |
tmpfile.seek(0)
|
| 474 |
gif_bytes = tmpfile.read()
|
| 475 |
+
st.image(gif_bytes, width='content', output_format="auto")
|
| 476 |
+
os.unlink(tmpfile.name)
|
| 477 |
|
| 478 |
|
| 479 |
def _render_grid(state: GameState, letter_map):
|