File size: 5,932 Bytes
d747bc4
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
581c2fc
 
d747bc4
 
 
 
581c2fc
d747bc4
 
 
 
 
 
 
 
 
 
 
 
 
 
581c2fc
d747bc4
 
 
 
 
 
 
581c2fc
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
d747bc4
 
 
 
 
 
 
 
 
 
581c2fc
 
d747bc4
 
 
581c2fc
d747bc4
 
 
 
 
 
581c2fc
d747bc4
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
from __future__ import annotations

from typing import Optional

import matplotlib.pyplot as plt
import streamlit as st

from .generator import generate_puzzle, load_word_list
from .logic import build_letter_map, reveal_cell, guess_word, is_game_over, compute_tier
from .models import Coord, GameState, Puzzle


def _init_session() -> None:
    if "initialized" in st.session_state and st.session_state.initialized:
        return

    words = load_word_list()
    puzzle = generate_puzzle(grid_size=12, words_by_len=words)

    st.session_state.puzzle = puzzle
    st.session_state.grid_size = 12
    st.session_state.revealed = set()
    st.session_state.guessed = set()
    st.session_state.score = 0
    st.session_state.last_action = "Welcome to Battlewords! Reveal a cell to begin."
    st.session_state.can_guess = False
    st.session_state.points_by_word = {}
    st.session_state.letter_map = build_letter_map(puzzle)
    st.session_state.initialized = True


def _new_game() -> None:
    st.session_state.clear()
    _init_session()


def _to_state() -> GameState:
    return GameState(
        grid_size=st.session_state.grid_size,
        puzzle=st.session_state.puzzle,
        revealed=st.session_state.revealed,
        guessed=st.session_state.guessed,
        score=st.session_state.score,
        last_action=st.session_state.last_action,
        can_guess=st.session_state.can_guess,
        points_by_word=st.session_state.points_by_word,
    )


def _sync_back(state: GameState) -> None:
    st.session_state.revealed = state.revealed
    st.session_state.guessed = state.guessed
    st.session_state.score = state.score
    st.session_state.last_action = state.last_action
    st.session_state.can_guess = state.can_guess
    st.session_state.points_by_word = state.points_by_word


def _render_header():
    st.title("Battlewords (POC)")
    st.subheader("Reveal cells, then guess the hidden words.")
    st.markdown(
        "- Grid is 12×12 with 6 words (two 4-letter, two 5-letter, two 6-letter).\n"
        "- After each reveal, you may submit one guess.\n"
        "- Scoring: length + unrevealed letters of that word at guess time.")


def _render_sidebar():
    with st.sidebar:
        st.header("Controls")
        st.button("New Game", width="stretch", on_click=_new_game)
        st.markdown("Radar pulses show the last letter position of each hidden word.")


def _render_radar(puzzle: Puzzle, size: int):
    fig, ax = plt.subplots(figsize=(4, 4))
    xs = [c.y + 1 for c in puzzle.radar]  # columns on x-axis
    ys = [c.x + 1 for c in puzzle.radar]  # rows on y-axis
    ax.scatter(xs, ys, c="red", s=60, marker="o")
    ax.set_xlim(0.5, size + 0.5)
    ax.set_ylim(0.5, size + 0.5)
    ax.set_xticks(range(1, size + 1))
    ax.set_yticks(range(1, size + 1))
    ax.grid(True, which="both", linestyle="--", alpha=0.3)
    ax.set_title("Radar")
    st.pyplot(fig, width="stretch")
    plt.close(fig)


def _render_grid(state: GameState, letter_map):
    size = state.grid_size
    clicked: Optional[Coord] = None

    # Inject CSS for grid lines and button styling
    st.markdown(
        """
        <style>
        div[data-testid="column"] {
            padding: 0 !important;
        }
        button[data-testid="stButton"] {
            width: 32px !important;
            height: 32px !important;
            min-width: 32px !important;
            min-height: 32px !important;
            padding: 0 !important;
            margin: 0 !important;
            border: 1px solid #888 !important;
            background: #fff !important;
            font-weight: bold;
            font-size: 1rem;
        }
        </style>
        """,
        unsafe_allow_html=True,
    )

    grid_container = st.container()
    with grid_container:
        for r in range(size):
            cols = st.columns(size, gap="small")
            for c in range(size):
                coord = Coord(r, c)
                revealed = coord in state.revealed
                label = letter_map.get(coord, " ") if revealed else " "
                key = f"cell_{r}_{c}"
                if cols[c].button(label, key=key, help=f"({r+1},{c+1})"):
                    if not revealed:
                        clicked = coord

    if clicked is not None:
        reveal_cell(state, letter_map, clicked)
        st.session_state.letter_map = build_letter_map(st.session_state.puzzle)
        _sync_back(state)


def _render_guess_form(state: GameState):
    with st.form("guess_form"):
        guess_text = st.text_input("Your guess", value="", max_chars=12)
        submitted = st.form_submit_button("OK", disabled=not state.can_guess, width="stretch")
        if submitted:
            correct, _ = guess_word(state, guess_text)
            _sync_back(state)


def _render_score_panel(state: GameState):
    col1, col2 = st.columns([1, 3])
    with col1:
        st.metric("Score", state.score)
    with col2:
        st.markdown(f"Last action: {state.last_action}")


def _render_game_over(state: GameState):
    st.subheader("Game Over")
    tier = compute_tier(state.score)
    st.markdown(f"Final score: {state.score} — Tier: **{tier}**")

    with st.expander("Game summary", expanded=True):
        for w in state.puzzle.words:
            pts = state.points_by_word.get(w.text, 0)
            st.markdown(f"- {w.text} ({len(w.text)}): +{pts} points")
        st.markdown(f"**Total**: {state.score}")

    st.stop()


def run_app():
    _init_session()
    _render_header()
    _render_sidebar()

    state = _to_state()

    left, right = st.columns([3, 1], gap="large")
    with left:
        _render_grid(state, st.session_state.letter_map)
    with right:
        _render_radar(state.puzzle, size=state.grid_size)
        _render_score_panel(state)

    st.divider()
    _render_guess_form(state)

    # End condition
    state = _to_state()
    if is_game_over(state):
        _render_game_over(state)