Surn commited on
Commit
feb1c4e
·
1 Parent(s): 02e2dcd

UI Update 1

Browse files
Files changed (3) hide show
  1. app.py +1 -1
  2. battlewords/logic.py +10 -1
  3. battlewords/ui.py +101 -24
app.py CHANGED
@@ -10,7 +10,7 @@ def _new_game() -> None:
10
 
11
 
12
  def main():
13
- st.set_page_config(page_title="Battlewords (POC)", layout="wide")
14
  run_app()
15
 
16
 
 
10
 
11
 
12
  def main():
13
+ st.set_page_config(page_title="Battlewords (Proof Of Concept)", layout="wide")
14
  run_app()
15
 
16
 
battlewords/logic.py CHANGED
@@ -69,7 +69,16 @@ def guess_word(state: GameState, guess_text: str) -> Tuple[bool, int]:
69
 
70
 
71
  def is_game_over(state: GameState) -> bool:
72
- return len(state.guessed) == 6
 
 
 
 
 
 
 
 
 
73
 
74
 
75
  def compute_tier(score: int) -> str:
 
69
 
70
 
71
  def is_game_over(state: GameState) -> bool:
72
+ # Game ends if all words are guessed
73
+ if len(state.guessed) == 6:
74
+ return True
75
+ # Game ends if all word cells are revealed
76
+ all_word_cells = set()
77
+ for w in state.puzzle.words:
78
+ all_word_cells.update(w.cells)
79
+ if all_word_cells.issubset(state.revealed):
80
+ return True
81
+ return False
82
 
83
 
84
  def compute_tier(score: int) -> str:
battlewords/ui.py CHANGED
@@ -1,6 +1,6 @@
1
  from __future__ import annotations
2
-
3
- from typing import Optional
4
 
5
  import matplotlib.pyplot as plt
6
  import streamlit as st
@@ -42,6 +42,7 @@ def inject_styles() -> None:
42
  st.markdown(
43
  """
44
  <style>
 
45
  .bw-row { display: flex; gap: 4px; flex-wrap: nowrap; }
46
  .bw-cell {
47
  width: 100%;
@@ -53,37 +54,61 @@ def inject_styles() -> None:
53
  border-radius: 4px;
54
  font-weight: 700;
55
  user-select: none;
 
 
 
56
  }
57
  .bw-cell.letter { background: #1e1e1e; color: #eaeaea; }
58
  .bw-cell.empty { background: #0f0f0f; }
 
 
 
 
 
 
59
  div[data-testid="stButton"] button {
60
  width: 100%;
61
  aspect-ratio: 1 / 1;
62
  border-radius: 4px;
63
  border: 1px solid #3a3a3a;
64
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
65
  /* Mobile styles */
66
  @media (max-width: 640px) {
67
- /* stColumns: force 100% width and column-reverse for radar */
68
- div[data-testid="stHorizontalBlock"] {
69
- # flex-direction: column-reverse !important;
70
  width: 100% !important;
71
  max-width: 100vw !important;
72
  }
73
- div[data-testid="column"] {
74
  width: 100% !important;
75
  min-width: 100% !important;
76
- max-width: 100vw !important;
77
  flex: 1 1 100% !important;
78
  }
79
- div[data-testid="stLayoutWrapper"] .stHorizontalBlock div[data-testid="stColumn"]:first-child {
80
- min-width: calc(8.33333% - 1rem) !important;
81
- }
82
- /* Prevent grid from wrapping */
83
- .bw-row {
84
  flex-wrap: nowrap !important;
85
- width: 100vw !important;
86
- overflow-x: auto;
87
  }
88
  }
89
  </style>
@@ -139,12 +164,14 @@ def _sync_back(state: GameState) -> None:
139
 
140
 
141
  def _render_header():
142
- st.title("Battlewords (POC)")
143
  st.subheader("Reveal cells, then guess the hidden words.")
144
  st.markdown(
145
  "- Grid is 12×12 with 6 words (two 4-letter, two 5-letter, two 6-letter).\n"
146
- "- After each reveal, you may submit one guess.\n"
147
- "- Scoring: length + unrevealed letters of that word at guess time.")
 
 
148
  inject_styles()
149
 
150
 
@@ -152,16 +179,31 @@ def _render_sidebar():
152
  with st.sidebar:
153
  st.header("Controls")
154
  st.button("New Game", width="stretch", on_click=_new_game)
155
- st.markdown("Radar pulses show the last letter position of each hidden word.")
 
 
 
156
 
157
 
158
  def _render_radar(puzzle: Puzzle, size: int):
 
 
 
 
 
 
 
 
 
 
 
 
159
  fig, ax = plt.subplots(figsize=(4, 4))
160
  xs = [c.y + 1 for c in puzzle.radar] # columns on x-axis
161
  ys = [c.x + 1 for c in puzzle.radar] # rows on y-axis
162
  ax.scatter(xs, ys, c="red", s=60, marker="o")
163
- ax.set_xlim(0.5, size + 0.5)
164
- ax.set_ylim(0.5, size + 0.5)
165
  ax.set_xticks(range(1, size + 1))
166
  ax.set_yticks(range(1, size + 1))
167
  ax.grid(True, which="both", linestyle="--", alpha=0.3)
@@ -201,14 +243,43 @@ def _render_grid(state: GameState, letter_map):
201
  grid_container = st.container()
202
  with grid_container:
203
  for r in range(size):
 
 
204
  cols = st.columns(size, gap="small")
205
  for c in range(size):
206
  coord = Coord(r, c)
207
  revealed = coord in state.revealed
 
208
  label = letter_map.get(coord, " ") if revealed else " "
 
 
 
 
 
 
 
 
 
209
  key = f"cell_{r}_{c}"
210
- if cols[c].button(label, key=key, help=f"({r+1},{c+1})"):
211
- if not revealed:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
212
  clicked = coord
213
 
214
  if clicked is not None:
@@ -237,7 +308,11 @@ def _render_score_panel(state: GameState):
237
  def _render_game_over(state: GameState):
238
  st.subheader("Game Over")
239
  tier = compute_tier(state.score)
240
- st.markdown(f"Final score: {state.score} — Tier: **{tier}**")
 
 
 
 
241
 
242
  with st.expander("Game summary", expanded=True):
243
  for w in state.puzzle.words:
@@ -255,7 +330,9 @@ def run_app():
255
 
256
  state = _to_state()
257
 
258
- left, right = st.columns([3, 1], gap="large")
 
 
259
  with left:
260
  _render_grid(state, st.session_state.letter_map)
261
  with right:
 
1
  from __future__ import annotations
2
+ from . import __version__ as version
3
+ from typing import Iterable, Tuple, Optional
4
 
5
  import matplotlib.pyplot as plt
6
  import streamlit as st
 
42
  st.markdown(
43
  """
44
  <style>
45
+ /* Base grid cell visuals */
46
  .bw-row { display: flex; gap: 4px; flex-wrap: nowrap; }
47
  .bw-cell {
48
  width: 100%;
 
54
  border-radius: 4px;
55
  font-weight: 700;
56
  user-select: none;
57
+ padding: 0.25rem 0.75rem;
58
+ min-height: 2.5rem;
59
+ transition: background 0.2s ease;
60
  }
61
  .bw-cell.letter { background: #1e1e1e; color: #eaeaea; }
62
  .bw-cell.empty { background: #0f0f0f; }
63
+ .bw-cell.bw-cell-complete { background: #b7f7b7 !important; color: #1a1a1a !important; }
64
+
65
+ /* Final score style */
66
+ .bw-final-score { color: #1ca41c !important; font-weight: 800; }
67
+
68
+ /* Make grid buttons square and fill their column */
69
  div[data-testid="stButton"] button {
70
  width: 100%;
71
  aspect-ratio: 1 / 1;
72
  border-radius: 4px;
73
  border: 1px solid #3a3a3a;
74
  }
75
+
76
+ /* Ensure grid cell columns expand equally for both buttons and revealed cells */
77
+ div[data-testid="column"], .st-emotion-cache-zh2fnc {
78
+ width: auto !important;
79
+ flex: 1 1 auto !important;
80
+ min-width: 0 !important;
81
+ max-width: 100% !important;
82
+ }
83
+
84
+ /* Ensure grid rows generated via st.columns do not wrap and can scroll horizontally. */
85
+ .bw-grid-row-anchor + div[data-testid="stHorizontalBlock"] {
86
+ flex-wrap: nowrap !important;
87
+ overflow-x: auto !important;
88
+ }
89
+ .bw-grid-row-anchor + div[data-testid="stHorizontalBlock"] > div[data-testid="column"] {
90
+ flex: 0 0 auto !important;
91
+ }
92
+
93
  /* Mobile styles */
94
  @media (max-width: 640px) {
95
+ /* Reverse the main two-column layout (radar above grid) and force full width */
96
+ #bw-main-anchor + div[data-testid="stHorizontalBlock"] {
97
+ flex-direction: column-reverse !important;
98
  width: 100% !important;
99
  max-width: 100vw !important;
100
  }
101
+ #bw-main-anchor + div[data-testid="stHorizontalBlock"] > div[data-testid="column"] {
102
  width: 100% !important;
103
  min-width: 100% !important;
104
+ max-width: 100% !important;
105
  flex: 1 1 100% !important;
106
  }
107
+
108
+ /* Keep grid rows on one line on small screens too */
109
+ .bw-grid-row-anchor + div[data-testid="stHorizontalBlock"] {
 
 
110
  flex-wrap: nowrap !important;
111
+ overflow-x: auto !important;
 
112
  }
113
  }
114
  </style>
 
164
 
165
 
166
  def _render_header():
167
+ st.title(f"Battlewords (Proof Of Concept) v {version}")
168
  st.subheader("Reveal cells, then guess the hidden words.")
169
  st.markdown(
170
  "- Grid is 12×12 with 6 words (two 4-letter, two 5-letter, two 6-letter).\n"
171
+ "- After each reveal, you may submit one word guess below.\n"
172
+ "- Scoring: length + unrevealed letters of that word at guess time.\n"
173
+ "- Score Board: radar of last letter of word, score and status.\n"
174
+ "- Words do not overlap, but may be touching.")
175
  inject_styles()
176
 
177
 
 
179
  with st.sidebar:
180
  st.header("Controls")
181
  st.button("New Game", width="stretch", on_click=_new_game)
182
+ st.markdown(
183
+ "- Radar pulses show the last letter position of each hidden word.\n"
184
+ "- After each reveal, you may submit one word guess below.\n"
185
+ "- Scoring: length + unrevealed letters of that word at guess time.")
186
 
187
 
188
  def _render_radar(puzzle: Puzzle, size: int):
189
+ st.markdown(
190
+ """
191
+ <style>
192
+ h3 {
193
+ margin: 0.25rem auto;
194
+ text-align: center;
195
+ }
196
+ </style>
197
+ """,
198
+ unsafe_allow_html=True,
199
+ )
200
+ st.subheader("Score Board")
201
  fig, ax = plt.subplots(figsize=(4, 4))
202
  xs = [c.y + 1 for c in puzzle.radar] # columns on x-axis
203
  ys = [c.x + 1 for c in puzzle.radar] # rows on y-axis
204
  ax.scatter(xs, ys, c="red", s=60, marker="o")
205
+ ax.set_xlim(0.5, size)
206
+ ax.set_ylim(size, 0)
207
  ax.set_xticks(range(1, size + 1))
208
  ax.set_yticks(range(1, size + 1))
209
  ax.grid(True, which="both", linestyle="--", alpha=0.3)
 
243
  grid_container = st.container()
244
  with grid_container:
245
  for r in range(size):
246
+ # Anchor to style the following st.columns row container
247
+ st.markdown('<div class="bw-grid-row-anchor"></div>', unsafe_allow_html=True)
248
  cols = st.columns(size, gap="small")
249
  for c in range(size):
250
  coord = Coord(r, c)
251
  revealed = coord in state.revealed
252
+ # Get label if revealed
253
  label = letter_map.get(coord, " ") if revealed else " "
254
+
255
+ # If this coord belongs to a completed (guessed) word
256
+ is_completed_cell = False
257
+ if revealed:
258
+ for w in state.puzzle.words:
259
+ if w.text in state.guessed and coord in w.cells:
260
+ is_completed_cell = True
261
+ break
262
+
263
  key = f"cell_{r}_{c}"
264
+ tooltip = f"({r+1},{c+1})"
265
+
266
+ if is_completed_cell:
267
+ # Render a styled non-button cell with green background and native browser tooltip
268
+ safe_label = (label or " ")
269
+ cols[c].markdown(
270
+ f'<div class="bw-cell bw-cell-complete" title="{tooltip}">{safe_label}</div>',
271
+ unsafe_allow_html=True,
272
+ )
273
+ elif revealed:
274
+ # Render a styled non-button cell showing the letter with native browser tooltip
275
+ safe_label = (label or " ")
276
+ cols[c].markdown(
277
+ f'<div class="bw-cell letter" title="{tooltip}">{safe_label}</div>',
278
+ unsafe_allow_html=True,
279
+ )
280
+ else:
281
+ # Unrevealed: render a button to allow click/reveal with tooltip
282
+ if cols[c].button(" ", key=key, help=tooltip):
283
  clicked = coord
284
 
285
  if clicked is not None:
 
308
  def _render_game_over(state: GameState):
309
  st.subheader("Game Over")
310
  tier = compute_tier(state.score)
311
+ # Final score in green
312
+ st.markdown(
313
+ f"<span class=\"bw-final-score\">Final score: {state.score}</span> — Tier: <strong>{tier}</strong>",
314
+ unsafe_allow_html=True,
315
+ )
316
 
317
  with st.expander("Game summary", expanded=True):
318
  for w in state.puzzle.words:
 
330
 
331
  state = _to_state()
332
 
333
+ # Anchor to target the main two-column layout for mobile reversal
334
+ st.markdown('<div id="bw-main-anchor"></div>', unsafe_allow_html=True)
335
+ left, right = st.columns([3, 1], gap="medium")
336
  with left:
337
  _render_grid(state, st.session_state.letter_map)
338
  with right: