aephiday commited on
Commit
57f7495
Β·
verified Β·
1 Parent(s): f4be460

Update src/streamlit_app.py

Browse files
Files changed (1) hide show
  1. src/streamlit_app.py +416 -38
src/streamlit_app.py CHANGED
@@ -1,40 +1,418 @@
1
- import altair as alt
2
- import numpy as np
3
- import pandas as pd
4
  import streamlit as st
 
 
5
 
6
- """
7
- # Welcome to Streamlit!
8
-
9
- Edit `/streamlit_app.py` to customize this app to your heart's desire :heart:.
10
- If you have any questions, checkout our [documentation](https://docs.streamlit.io) and [community
11
- forums](https://discuss.streamlit.io).
12
-
13
- In the meantime, below is an example of what you can do with just a few lines of code:
14
- """
15
-
16
- num_points = st.slider("Number of points in spiral", 1, 10000, 1100)
17
- num_turns = st.slider("Number of turns in spiral", 1, 300, 31)
18
-
19
- indices = np.linspace(0, 1, num_points)
20
- theta = 2 * np.pi * num_turns * indices
21
- radius = indices
22
-
23
- x = radius * np.cos(theta)
24
- y = radius * np.sin(theta)
25
-
26
- df = pd.DataFrame({
27
- "x": x,
28
- "y": y,
29
- "idx": indices,
30
- "rand": np.random.randn(num_points),
31
- })
32
-
33
- st.altair_chart(alt.Chart(df, height=700, width=700)
34
- .mark_point(filled=True)
35
- .encode(
36
- x=alt.X("x", axis=None),
37
- y=alt.Y("y", axis=None),
38
- color=alt.Color("idx", legend=None, scale=alt.Scale()),
39
- size=alt.Size("rand", legend=None, scale=alt.Scale(range=[1, 150])),
40
- ))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  import streamlit as st
2
+ import random
3
+ from typing import List, Tuple, Dict, Optional
4
 
5
+ # Constants
6
+ SOLVED_STATE = [1, 2, 3, 4, 5, 6, 7, 8, 0]
7
+
8
+ class Puzzle8Game:
9
+ def __init__(self):
10
+ self.puzzle = None
11
+ self.initial_puzzle = None
12
+ self.moves = 0
13
+ self.game_won = False
14
+
15
+ @staticmethod
16
+ def pos_to_coords(pos: int) -> Dict[str, int]:
17
+ """Convert position (1-9) to row, col coordinates (1-3)"""
18
+ return {"row": (pos - 1) // 3 + 1, "col": (pos - 1) % 3 + 1}
19
+
20
+ @staticmethod
21
+ def coords_to_pos(row: int, col: int) -> int:
22
+ """Convert row, col coordinates to position"""
23
+ return (row - 1) * 3 + col
24
+
25
+ @staticmethod
26
+ def is_valid_position(row: int, col: int) -> bool:
27
+ """Check if position is within 3x3 grid"""
28
+ return 1 <= row <= 3 and 1 <= col <= 3
29
+
30
+ def create_solvable_puzzle(self, moves: int = 100) -> List[int]:
31
+ """Create a solvable puzzle by shuffling from solved state"""
32
+ puzzle = SOLVED_STATE.copy()
33
+ blank_pos = 9
34
+
35
+ # Simple shuffling with valid moves
36
+ for _ in range(moves):
37
+ coords = self.pos_to_coords(blank_pos)
38
+ valid_moves = []
39
+
40
+ # Check all four directions
41
+ directions = [
42
+ {"row": -1, "col": 0}, # Up
43
+ {"row": 1, "col": 0}, # Down
44
+ {"row": 0, "col": -1}, # Left
45
+ {"row": 0, "col": 1} # Right
46
+ ]
47
+
48
+ for direction in directions:
49
+ new_row = coords["row"] + direction["row"]
50
+ new_col = coords["col"] + direction["col"]
51
+
52
+ if self.is_valid_position(new_row, new_col):
53
+ valid_moves.append(self.coords_to_pos(new_row, new_col))
54
+
55
+ if valid_moves:
56
+ move_pos = random.choice(valid_moves)
57
+
58
+ # Swap tiles
59
+ puzzle[blank_pos - 1], puzzle[move_pos - 1] = puzzle[move_pos - 1], puzzle[blank_pos - 1]
60
+ blank_pos = move_pos
61
+
62
+ # Ensure puzzle is not already solved
63
+ if puzzle == SOLVED_STATE:
64
+ return self.create_solvable_puzzle(moves + 20)
65
+
66
+ return puzzle
67
+
68
+ def is_solved(self, puzzle: List[int]) -> bool:
69
+ """Check if puzzle is solved"""
70
+ return puzzle == SOLVED_STATE
71
+
72
+ def get_blank_position(self, puzzle: List[int]) -> int:
73
+ """Get position of blank tile (value 0)"""
74
+ return puzzle.index(0) + 1
75
+
76
+ def get_valid_moves(self, blank_pos: int) -> List[int]:
77
+ """Get valid moves for current blank position"""
78
+ coords = self.pos_to_coords(blank_pos)
79
+ valid_moves = []
80
+
81
+ directions = [
82
+ {"row": -1, "col": 0}, # Up
83
+ {"row": 1, "col": 0}, # Down
84
+ {"row": 0, "col": -1}, # Left
85
+ {"row": 0, "col": 1} # Right
86
+ ]
87
+
88
+ for direction in directions:
89
+ new_row = coords["row"] + direction["row"]
90
+ new_col = coords["col"] + direction["col"]
91
+
92
+ if self.is_valid_position(new_row, new_col):
93
+ valid_moves.append(self.coords_to_pos(new_row, new_col))
94
+
95
+ return valid_moves
96
+
97
+ def move_tile(self, puzzle: List[int], tile_position: int) -> Dict:
98
+ """Move tile with validation"""
99
+ blank_pos = self.get_blank_position(puzzle)
100
+ valid_moves = self.get_valid_moves(blank_pos)
101
+
102
+ if tile_position in valid_moves:
103
+ # Create new puzzle state
104
+ new_puzzle = puzzle.copy()
105
+ new_puzzle[blank_pos - 1] = puzzle[tile_position - 1]
106
+ new_puzzle[tile_position - 1] = 0
107
+
108
+ return {"puzzle": new_puzzle, "moved": True}
109
+
110
+ return {"puzzle": puzzle, "moved": False}
111
+
112
+ def new_game(self):
113
+ """Start a new game"""
114
+ new_puzzle = self.create_solvable_puzzle()
115
+ self.puzzle = new_puzzle
116
+ self.initial_puzzle = new_puzzle.copy()
117
+ self.moves = 0
118
+ self.game_won = False
119
+
120
+ def reset_game(self):
121
+ """Reset to initial puzzle state"""
122
+ if self.initial_puzzle:
123
+ self.puzzle = self.initial_puzzle.copy()
124
+ self.moves = 0
125
+ self.game_won = False
126
+
127
+ def make_move(self, position: int):
128
+ """Make a move and update game state"""
129
+ if not self.game_won and self.puzzle:
130
+ result = self.move_tile(self.puzzle, position)
131
+
132
+ if result["moved"]:
133
+ self.puzzle = result["puzzle"]
134
+ self.moves += 1
135
+
136
+ # Check win condition
137
+ if self.is_solved(self.puzzle):
138
+ self.game_won = True
139
+
140
+ # Initialize game in session state
141
+ if 'game' not in st.session_state:
142
+ st.session_state.game = Puzzle8Game()
143
+ st.session_state.game.new_game()
144
+
145
+ # Custom CSS for styling
146
+ st.markdown("""
147
+ <style>
148
+ .main > div {
149
+ padding-top: 2rem;
150
+ padding-bottom: 2rem;
151
+ }
152
+
153
+ .game-container {
154
+ background: rgba(255, 255, 255, 0.95);
155
+ border-radius: 16px;
156
+ box-shadow: 0 20px 40px rgba(0,0,0,0.1);
157
+ padding: 30px;
158
+ margin: 20px auto;
159
+ text-align: center;
160
+ max-width: 600px;
161
+ }
162
+
163
+ .puzzle-grid {
164
+ display: grid;
165
+ grid-template-columns: repeat(3, 100px);
166
+ grid-template-rows: repeat(3, 100px);
167
+ gap: 8px;
168
+ justify-content: center;
169
+ margin: 30px auto;
170
+ padding: 20px;
171
+ background: rgba(0,0,0,0.05);
172
+ border-radius: 8px;
173
+ }
174
+
175
+ .puzzle-tile {
176
+ width: 100px;
177
+ height: 100px;
178
+ border: none;
179
+ border-radius: 8px;
180
+ background: linear-gradient(145deg, #2196F3, #1976D2);
181
+ color: white;
182
+ font-size: 24px;
183
+ font-weight: 700;
184
+ display: flex;
185
+ align-items: center;
186
+ justify-content: center;
187
+ cursor: pointer;
188
+ transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
189
+ box-shadow: 0 2px 4px rgba(0,0,0,0.1);
190
+ }
191
+
192
+ .puzzle-tile:hover {
193
+ transform: translateY(-2px) scale(1.02);
194
+ box-shadow: 0 4px 8px rgba(0,0,0,0.15);
195
+ }
196
+
197
+ .empty-tile {
198
+ background: rgba(255,255,255,0.8);
199
+ border: 2px dashed #ccc;
200
+ box-shadow: inset 0 2px 4px rgba(0,0,0,0.1);
201
+ }
202
+
203
+ .moves-counter {
204
+ background: white;
205
+ padding: 20px;
206
+ border-radius: 8px;
207
+ margin: 20px 0;
208
+ box-shadow: 0 2px 4px rgba(0,0,0,0.1);
209
+ border-left: 4px solid #2196F3;
210
+ }
211
+
212
+ .congratulations-card {
213
+ background: linear-gradient(145deg, #4CAF50, #388E3C);
214
+ color: white;
215
+ padding: 25px;
216
+ border-radius: 8px;
217
+ margin: 20px 0;
218
+ box-shadow: 0 4px 8px rgba(0,0,0,0.15);
219
+ animation: celebration 0.6s ease;
220
+ }
221
+
222
+ @keyframes celebration {
223
+ 0% {
224
+ opacity: 0;
225
+ transform: translateY(-20px) scale(0.9);
226
+ }
227
+ 50% {
228
+ transform: translateY(-5px) scale(1.05);
229
+ }
230
+ 100% {
231
+ opacity: 1;
232
+ transform: translateY(0) scale(1);
233
+ }
234
+ }
235
+
236
+ .moves-value {
237
+ font-size: 36px;
238
+ font-weight: bold;
239
+ color: #2196F3;
240
+ margin-bottom: 5px;
241
+ }
242
+
243
+ .moves-label {
244
+ font-size: 14px;
245
+ color: #666;
246
+ text-transform: uppercase;
247
+ letter-spacing: 1px;
248
+ }
249
+
250
+ .congratulations-title {
251
+ font-size: 28px;
252
+ font-weight: bold;
253
+ margin-bottom: 10px;
254
+ }
255
+
256
+ .congratulations-subtitle {
257
+ font-size: 18px;
258
+ font-weight: 600;
259
+ opacity: 0.9;
260
+ }
261
+
262
+ .stButton > button {
263
+ width: 140px;
264
+ height: 45px;
265
+ border-radius: 8px;
266
+ font-weight: 600;
267
+ font-size: 16px;
268
+ text-transform: uppercase;
269
+ letter-spacing: 0.5px;
270
+ margin: 5px;
271
+ transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
272
+ }
273
+
274
+ .stButton > button:hover {
275
+ transform: translateY(-2px);
276
+ box-shadow: 0 4px 8px rgba(0,0,0,0.15);
277
+ }
278
+
279
+ .instructions {
280
+ background: white;
281
+ border-radius: 8px;
282
+ padding: 25px;
283
+ box-shadow: 0 2px 4px rgba(0,0,0,0.1);
284
+ text-align: left;
285
+ margin: 20px 0;
286
+ }
287
+
288
+ .goal-grid {
289
+ display: grid;
290
+ grid-template-columns: repeat(3, 40px);
291
+ gap: 4px;
292
+ justify-content: center;
293
+ margin: 15px 0;
294
+ }
295
+
296
+ .goal-tile {
297
+ width: 40px;
298
+ height: 40px;
299
+ background: #2196F3;
300
+ color: white;
301
+ display: flex;
302
+ align-items: center;
303
+ justify-content: center;
304
+ border-radius: 4px;
305
+ font-weight: bold;
306
+ font-size: 14px;
307
+ }
308
+
309
+ .goal-empty {
310
+ background: #f0f0f0;
311
+ border: 2px dashed #ccc;
312
+ }
313
+
314
+ /* Hide Streamlit elements */
315
+ #MainMenu {visibility: hidden;}
316
+ footer {visibility: hidden;}
317
+ header {visibility: hidden;}
318
+ </style>
319
+ """, unsafe_allow_html=True)
320
+
321
+ # Page title
322
+ st.markdown("<h1 style='text-align: center; background: linear-gradient(145deg, #2196F3, #1976D2); -webkit-background-clip: text; -webkit-text-fill-color: transparent; font-weight: 800; font-size: 3rem; margin-bottom: 30px;'>🧩 Puzzle8</h1>", unsafe_allow_html=True)
323
+
324
+ # Game container
325
+ game = st.session_state.game
326
+
327
+ # Status card (moves counter or congratulations)
328
+ if game.game_won:
329
+ st.markdown(f"""
330
+ <div class="congratulations-card">
331
+ <div class="congratulations-title">πŸ† Congratulations!</div>
332
+ <div class="congratulations-subtitle">Puzzle solved in {game.moves} moves!</div>
333
+ </div>
334
+ """, unsafe_allow_html=True)
335
+ else:
336
+ st.markdown(f"""
337
+ <div class="moves-counter">
338
+ <div class="moves-value">{game.moves}</div>
339
+ <div class="moves-label">Moves</div>
340
+ </div>
341
+ """, unsafe_allow_html=True)
342
+
343
+ # Puzzle grid
344
+ st.markdown('<div class="puzzle-grid">', unsafe_allow_html=True)
345
+
346
+ # Create 3x3 grid of tiles
347
+ cols = st.columns(3)
348
+ for i in range(9):
349
+ col_idx = i % 3
350
+
351
+ with cols[col_idx]:
352
+ if game.puzzle and len(game.puzzle) > i:
353
+ value = game.puzzle[i]
354
+
355
+ if value == 0:
356
+ # Empty tile
357
+ st.markdown('<div class="empty-tile puzzle-tile"></div>', unsafe_allow_html=True)
358
+ else:
359
+ # Number tile - make it clickable
360
+ if st.button(str(value), key=f"tile_{i+1}", help=f"Move tile {value}"):
361
+ game.make_move(i + 1)
362
+ st.rerun()
363
+
364
+ st.markdown('</div>', unsafe_allow_html=True)
365
+
366
+ # Control buttons
367
+ col1, col2 = st.columns(2)
368
+
369
+ with col1:
370
+ if st.button("🎲 New Game", key="new_game", type="primary"):
371
+ game.new_game()
372
+ st.rerun()
373
+
374
+ with col2:
375
+ if st.button("πŸ”„ Reset", key="reset_game", type="secondary"):
376
+ game.reset_game()
377
+ st.rerun()
378
+
379
+ # Instructions
380
+ with st.expander("🎯 How to Play", expanded=False):
381
+ st.markdown("""
382
+ **Goal:** Arrange numbers 1-8 in order with the empty space in the bottom-right corner.
383
+
384
+ **Rules:**
385
+ - Click on tiles adjacent to the empty space to move them
386
+ - Only tiles next to the empty space can be moved
387
+ - Try to solve the puzzle in as few moves as possible!
388
+
389
+ **Target arrangement:**
390
+ """)
391
+
392
+ # Goal grid visualization
393
+ st.markdown("""
394
+ <div style="text-align: center;">
395
+ <div class="goal-grid">
396
+ <div class="goal-tile">1</div>
397
+ <div class="goal-tile">2</div>
398
+ <div class="goal-tile">3</div>
399
+ <div class="goal-tile">4</div>
400
+ <div class="goal-tile">5</div>
401
+ <div class="goal-tile">6</div>
402
+ <div class="goal-tile">7</div>
403
+ <div class="goal-tile">8</div>
404
+ <div class="goal-tile goal-empty"></div>
405
+ </div>
406
+ </div>
407
+ """, unsafe_allow_html=True)
408
+
409
+ # Debug info (optional - can be removed)
410
+ if st.checkbox("Show Debug Info", value=False):
411
+ st.write("Current puzzle state:", game.puzzle)
412
+ st.write("Moves:", game.moves)
413
+ st.write("Game won:", game.game_won)
414
+ if game.puzzle:
415
+ blank_pos = game.get_blank_position(game.puzzle)
416
+ valid_moves = game.get_valid_moves(blank_pos)
417
+ st.write("Blank position:", blank_pos)
418
+ st.write("Valid moves:", valid_moves)