shachar commited on
Commit
d3e9649
·
1 Parent(s): 412c3b6

bug fixes; added choosing random opening, score+completed

Browse files
Files changed (4) hide show
  1. README.md +30 -3
  2. app.py +158 -22
  3. requirements.txt +1 -1
  4. style.css +10 -0
README.md CHANGED
@@ -9,8 +9,35 @@ app_file: app.py
9
  pinned: false
10
  license: cc0-1.0
11
  datasets:
12
- - Lichess/chess-openings
13
- suggested_hardware: cpu-basic
 
14
  ---
15
 
16
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9
  pinned: false
10
  license: cc0-1.0
11
  datasets:
12
+
13
+ - Lichess/chess-openings
14
+ suggested_hardware: cpu-basic
15
  ---
16
 
17
+ # Chess Openings
18
+
19
+ This little app lets you practice ~3500 chess openings.
20
+
21
+ This is just a toy app A(I) wrote for fun. Go to [Lichess](https://lichess.org/) or [Chess.com](https://chess.com) for serious chess practice.
22
+
23
+ ## Entering moves
24
+
25
+ - Use standard algebraic notation (SAN)
26
+ - Examples: e4, Nf3, O-O (castling), exd5 (pawn capture)
27
+ - Specify the piece (except for pawns) + destination square
28
+ - Use 'x' for captures, '+' for check, '#' for checkmate
29
+
30
+ ## Piece symbols
31
+
32
+ - ♔ King (K)
33
+ - ♕ Queen (Q)
34
+ - ♖ Rook (R)
35
+ - ♗ Bishop (B)
36
+ - ♘ Knight (N)
37
+ - ♙ Pawn (no letter)
38
+
39
+ See full notation [here](<https://en.wikipedia.org/wiki/Algebraic_notation_(chess)>)
40
+
41
+ ## Dataset
42
+
43
+ This app is using the [Lichess](https://lichess.org/) openings dataset via [HuggingFace](https://huggingface.co/datasets/Lichess/chess-openings)
app.py CHANGED
@@ -1,4 +1,5 @@
1
  import io
 
2
 
3
  import chess
4
  import chess.pgn
@@ -8,6 +9,10 @@ from datasets import load_dataset
8
 
9
  st.set_page_config(page_title="Practice Chess Openings", page_icon="♖")
10
 
 
 
 
 
11
 
12
  @st.cache_data
13
  def load_data():
@@ -29,15 +34,38 @@ if "board" not in st.session_state:
29
  st.session_state.board = chess.Board()
30
  if "moves" not in st.session_state:
31
  st.session_state.moves = []
 
 
 
 
 
 
 
 
32
 
33
  data = load_data()
34
-
35
  if data.empty:
36
  st.error("No data available. Failed to load from Hugging Face dataset.")
37
  st.stop()
38
 
 
 
 
 
 
 
 
 
 
 
 
 
 
39
  # App layout
40
- st.title("Practice Chess Openings")
 
 
 
41
 
42
  with st.sidebar:
43
  st.header("Settings")
@@ -64,10 +92,47 @@ with st.sidebar:
64
  )
65
  st.stop()
66
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
67
  opening = st.selectbox(
68
  label="Select an opening",
69
- options=list(filtered_data["name"].unique()),
70
- index=None,
 
 
71
  key="opening_selector",
72
  placeholder="Select an opening",
73
  )
@@ -77,16 +142,18 @@ with st.sidebar:
77
  st.session_state.move_index = 0
78
  st.session_state.user_move = ""
79
  st.session_state.board = chess.Board()
 
 
 
80
 
81
- # Get PGN for the selected opening
82
  selected_opening = filtered_data[filtered_data["name"] == opening].iloc[0]
83
  pgn = selected_opening["pgn"]
84
-
85
- # Parse PGN
86
  game = chess.pgn.read_game(io.StringIO(pgn))
87
  st.session_state.moves = list(game.mainline_moves())
88
 
89
  with st.expander("Instructions"):
 
90
  st.write("Entering moves:")
91
  st.write("Use standard algebraic notation (SAN)")
92
  st.write("Examples: e4, Nf3, O-O (castling), exd5 (pawn capture)")
@@ -108,9 +175,15 @@ with st.sidebar:
108
  st.write(
109
  "See full notation [here](https://en.wikipedia.org/wiki/Algebraic_notation_(chess))"
110
  )
 
111
  st.write(
112
  "This app is using the [Lichess](https://lichess.org/) openings dataset via [HuggingFace](https://huggingface.co/datasets/Lichess/chess-openings)"
113
  )
 
 
 
 
 
114
 
115
 
116
  def update_board():
@@ -128,6 +201,9 @@ def update_next_move():
128
  def update_prev_move():
129
  if st.session_state.move_index > 0:
130
  st.session_state.move_index -= 1
 
 
 
131
  update_board()
132
 
133
 
@@ -136,31 +212,50 @@ col1, col2 = st.columns([3, 1])
136
 
137
  with col1:
138
  if st.session_state.current_opening:
139
- st.subheader(f":blue[{st.session_state.current_opening}]")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
140
 
141
- col_prev, col_next, right_col = st.columns([1, 1, 1])
142
 
143
  with col_prev:
144
  st.button(
145
- "⬅️ Previous",
146
  disabled=st.session_state.move_index == 0,
147
  on_click=update_prev_move,
148
  )
149
  with col_next:
150
  st.button(
151
- "➡️ Next",
152
- disabled=st.session_state.move_index >= len(st.session_state.moves),
 
 
 
 
153
  on_click=update_next_move,
154
  )
155
 
156
  board_container = st.empty()
157
- board_container.image(chess.svg.board(board=st.session_state.board, size=450))
158
 
159
  # User input for next move
160
  if hide_next_moves and st.session_state.move_index < len(
161
  st.session_state.moves
162
  ):
163
- col_input, col_submit, col_right = st.columns([2, 1, 1])
164
 
165
  def submit_move():
166
  if st.session_state.user_move.strip() == "":
@@ -170,14 +265,23 @@ with col1:
170
  user_chess_move = st.session_state.board.parse_san(user_move)
171
  correct_move = st.session_state.moves[st.session_state.move_index]
172
  if user_chess_move == correct_move:
173
- st.session_state.success_message = (
174
- "Correct move! Moving to the next one."
175
- )
176
  st.session_state.move_index += 1
 
 
 
 
 
 
 
 
 
 
 
177
  st.session_state.user_move = ""
178
  update_board()
179
  else:
180
- st.session_state.error_message = "Incorrect move. Try again!"
181
  except ValueError as e:
182
  error_message = str(e).lower()
183
  if (
@@ -185,9 +289,9 @@ with col1:
185
  or "unexpected" in error_message
186
  or "unterminated" in error_message
187
  ):
188
- st.session_state.error_message = "Invalid move format. Please use standard SAN notation (e.g., e4 or Nf3)."
189
  else:
190
- st.session_state.error_message = "Invalid move. This move is not allowed in the current position."
191
 
192
  with col_input:
193
  user_move = st.text_input(
@@ -203,7 +307,8 @@ with col1:
203
  del st.session_state.error_message
204
  elif "success_message" in st.session_state:
205
  st.success(st.session_state.success_message)
206
- del st.session_state.success_message
 
207
 
208
  with col_submit:
209
  st.markdown("<br>", unsafe_allow_html=True)
@@ -211,12 +316,18 @@ with col1:
211
  "Submit",
212
  on_click=submit_move,
213
  )
 
 
 
 
 
 
214
  else:
215
  st.info("Please select an opening from the sidebar to begin.")
216
 
217
  with col2:
218
  if st.session_state.current_opening:
219
- st.header("Moves", divider="green")
220
  move_text = ""
221
  current_node = chess.pgn.Game()
222
  for i, move in enumerate(st.session_state.moves):
@@ -234,3 +345,28 @@ with col2:
234
  move_text += "\n"
235
  current_node = current_node.add_variation(move)
236
  st.markdown(move_text)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  import io
2
+ import random
3
 
4
  import chess
5
  import chess.pgn
 
9
 
10
  st.set_page_config(page_title="Practice Chess Openings", page_icon="♖")
11
 
12
+ # Load external CSS
13
+ with open("style.css") as f:
14
+ st.markdown(f"<style>{f.read()}</style>", unsafe_allow_html=True)
15
+
16
 
17
  @st.cache_data
18
  def load_data():
 
34
  st.session_state.board = chess.Board()
35
  if "moves" not in st.session_state:
36
  st.session_state.moves = []
37
+ if "final_move_completed" not in st.session_state:
38
+ st.session_state.final_move_completed = False
39
+ if "random_opening" not in st.session_state:
40
+ st.session_state.random_opening = None
41
+ if "completed_openings" not in st.session_state:
42
+ st.session_state.completed_openings = set()
43
+ if "score" not in st.session_state:
44
+ st.session_state.score = 0
45
 
46
  data = load_data()
 
47
  if data.empty:
48
  st.error("No data available. Failed to load from Hugging Face dataset.")
49
  st.stop()
50
 
51
+
52
+ def update_score():
53
+ # Note: keeping it as a function to be able to try different scores
54
+ st.session_state.score += 1
55
+ # score = 0
56
+ # for opening in st.session_state.completed_openings:
57
+ # opening_data = data[data["name"] == opening].iloc[0]
58
+ # moves_count = opening_data["pgn"].count(".")
59
+ # score += moves_count
60
+
61
+ # return score
62
+
63
+
64
  # App layout
65
+ st.markdown(
66
+ "<h1 style='text-align: center; font-size: 32px; margin-bottom: 10px; margin-top: -25px; padding-top: 0; text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.5);'>Practice Chess Openings</h1>",
67
+ unsafe_allow_html=True,
68
+ )
69
 
70
  with st.sidebar:
71
  st.header("Settings")
 
92
  )
93
  st.stop()
94
 
95
+ # Random Opening button with custom styling
96
+ random_button = st.button(
97
+ "Random Opening",
98
+ key="random_opening_button",
99
+ use_container_width=True,
100
+ help="Select a random opening",
101
+ type="primary", # This will give it a filled style
102
+ )
103
+
104
+ # Remove the previous CSS styling attempt
105
+ # The CSS is now loaded from the external file
106
+
107
+ if random_button:
108
+ available_openings = [
109
+ opening
110
+ for opening in filtered_data["name"].unique()
111
+ if opening not in st.session_state.completed_openings
112
+ ]
113
+ if available_openings:
114
+ st.session_state.random_opening = random.choice(available_openings)
115
+ else:
116
+ st.warning(
117
+ "You've completed all available openings! Resetting completed list."
118
+ )
119
+ st.session_state.completed_openings.clear()
120
+ st.session_state.random_opening = random.choice(
121
+ list(filtered_data["name"].unique())
122
+ )
123
+ st.rerun()
124
+
125
+ # Check if the random_opening is still in the filtered data
126
+ unique_openings = list(filtered_data["name"].unique())
127
+ if st.session_state.random_opening not in unique_openings:
128
+ st.session_state.random_opening = None
129
+
130
  opening = st.selectbox(
131
  label="Select an opening",
132
+ options=unique_openings,
133
+ index=unique_openings.index(st.session_state.random_opening)
134
+ if st.session_state.random_opening
135
+ else 0,
136
  key="opening_selector",
137
  placeholder="Select an opening",
138
  )
 
142
  st.session_state.move_index = 0
143
  st.session_state.user_move = ""
144
  st.session_state.board = chess.Board()
145
+ st.session_state.final_move_completed = False
146
+ if "success_message" in st.session_state:
147
+ del st.session_state.success_message
148
 
149
+ # Get PGN for the selected opening and parse it
150
  selected_opening = filtered_data[filtered_data["name"] == opening].iloc[0]
151
  pgn = selected_opening["pgn"]
 
 
152
  game = chess.pgn.read_game(io.StringIO(pgn))
153
  st.session_state.moves = list(game.mainline_moves())
154
 
155
  with st.expander("Instructions"):
156
+ st.write("This app lets you practice ~3500 chess openings.")
157
  st.write("Entering moves:")
158
  st.write("Use standard algebraic notation (SAN)")
159
  st.write("Examples: e4, Nf3, O-O (castling), exd5 (pawn capture)")
 
175
  st.write(
176
  "See full notation [here](https://en.wikipedia.org/wiki/Algebraic_notation_(chess))"
177
  )
178
+ st.write("---")
179
  st.write(
180
  "This app is using the [Lichess](https://lichess.org/) openings dataset via [HuggingFace](https://huggingface.co/datasets/Lichess/chess-openings)"
181
  )
182
+ st.write(
183
+ "This is just a toy app. Go to [Lichess](https://lichess.org/) \
184
+ or [Chess.com](https://chess.com) for serious chess practice \
185
+ (although I think this functionality isn't available there)"
186
+ )
187
 
188
 
189
  def update_board():
 
201
  def update_prev_move():
202
  if st.session_state.move_index > 0:
203
  st.session_state.move_index -= 1
204
+ st.session_state.final_move_completed = False
205
+ if "success_message" in st.session_state:
206
+ del st.session_state.success_message
207
  update_board()
208
 
209
 
 
212
 
213
  with col1:
214
  if st.session_state.current_opening:
215
+ st.markdown(
216
+ f"""
217
+ <div style='width: 360px; margin-bottom: 10px;'>
218
+ <h3 style='
219
+ background: linear-gradient(90deg, #0077be, #00a86b);
220
+ -webkit-background-clip: text;
221
+ -webkit-text-fill-color: transparent;
222
+ display: inline-block;
223
+ font-weight: bold;
224
+ '>
225
+ {st.session_state.current_opening}
226
+ </h3>
227
+ </div>
228
+ """,
229
+ unsafe_allow_html=True,
230
+ )
231
 
232
+ col_prev, col_next, right_col = st.columns([5, 2, 2])
233
 
234
  with col_prev:
235
  st.button(
236
+ "⬅️ &nbsp; Previous",
237
  disabled=st.session_state.move_index == 0,
238
  on_click=update_prev_move,
239
  )
240
  with col_next:
241
  st.button(
242
+ "Next &nbsp;&nbsp;&nbsp;➡️",
243
+ disabled=(st.session_state.move_index >= len(st.session_state.moves))
244
+ or (
245
+ not hide_next_moves
246
+ and st.session_state.move_index == len(st.session_state.moves)
247
+ ),
248
  on_click=update_next_move,
249
  )
250
 
251
  board_container = st.empty()
252
+ board_container.image(chess.svg.board(board=st.session_state.board, size=400))
253
 
254
  # User input for next move
255
  if hide_next_moves and st.session_state.move_index < len(
256
  st.session_state.moves
257
  ):
258
+ col_input, col_submit, col_right = st.columns([3, 1, 1])
259
 
260
  def submit_move():
261
  if st.session_state.user_move.strip() == "":
 
265
  user_chess_move = st.session_state.board.parse_san(user_move)
266
  correct_move = st.session_state.moves[st.session_state.move_index]
267
  if user_chess_move == correct_move:
268
+ update_score()
 
 
269
  st.session_state.move_index += 1
270
+ if st.session_state.move_index == len(st.session_state.moves):
271
+ st.session_state.success_message = "🎉 &nbsp; Well done!"
272
+ st.session_state.final_move_completed = True
273
+ st.session_state.completed_openings.add(
274
+ st.session_state.current_opening
275
+ )
276
+ else:
277
+ st.session_state.success_message = (
278
+ "✅ Correct! Moving to the next one"
279
+ )
280
+
281
  st.session_state.user_move = ""
282
  update_board()
283
  else:
284
+ st.session_state.error_message = "😭 Incorrect move. Try again!"
285
  except ValueError as e:
286
  error_message = str(e).lower()
287
  if (
 
289
  or "unexpected" in error_message
290
  or "unterminated" in error_message
291
  ):
292
+ st.session_state.error_message = "🚫 Invalid format. Please use standard SAN notation (e.g., e4 or Nf3)."
293
  else:
294
+ st.session_state.error_message = "Invalid move. This move is not allowed in the current position."
295
 
296
  with col_input:
297
  user_move = st.text_input(
 
307
  del st.session_state.error_message
308
  elif "success_message" in st.session_state:
309
  st.success(st.session_state.success_message)
310
+ if not st.session_state.final_move_completed:
311
+ del st.session_state.success_message
312
 
313
  with col_submit:
314
  st.markdown("<br>", unsafe_allow_html=True)
 
316
  "Submit",
317
  on_click=submit_move,
318
  )
319
+
320
+ if st.session_state.final_move_completed:
321
+ col_success, col_empty = st.columns([1, 2])
322
+ with col_success:
323
+ st.success("🎉 &nbsp; Well done!")
324
+
325
  else:
326
  st.info("Please select an opening from the sidebar to begin.")
327
 
328
  with col2:
329
  if st.session_state.current_opening:
330
+ st.subheader("Moves", divider="green")
331
  move_text = ""
332
  current_node = chess.pgn.Game()
333
  for i, move in enumerate(st.session_state.moves):
 
345
  move_text += "\n"
346
  current_node = current_node.add_variation(move)
347
  st.markdown(move_text)
348
+
349
+ with st.sidebar:
350
+ st.markdown("---")
351
+
352
+ # score = update_score()
353
+ st.metric(
354
+ label="Score",
355
+ value=st.session_state.score,
356
+ help="1 point per correct move",
357
+ )
358
+
359
+ col1, col2 = st.columns([3, 1])
360
+ with col1:
361
+ st.subheader("Completed Openings")
362
+ with col2:
363
+ if st.button("Reset", key="reset_completed", help="Reset completed openings"):
364
+ st.session_state.completed_openings.clear()
365
+ st.rerun()
366
+
367
+ with st.expander("View Completed Openings"):
368
+ if st.session_state.completed_openings:
369
+ for completed_opening in st.session_state.completed_openings:
370
+ st.markdown(f"- {completed_opening}")
371
+ else:
372
+ st.markdown("No openings completed yet.")
requirements.txt CHANGED
@@ -1,4 +1,4 @@
1
  pandas>=2.2.3
2
  python-chess>=1.2.0
3
  datasets>=3.0.0
4
-
 
1
  pandas>=2.2.3
2
  python-chess>=1.2.0
3
  datasets>=3.0.0
4
+ streamlit>=1.38.0
style.css ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ div[data-testid="element-container"]:has(button[kind="primary"]) button[kind="primary"] {
2
+ background-color: #9f1b4d;
3
+ color: white;
4
+ font-weight: bold;
5
+ border: none;
6
+ }
7
+ div[data-testid="element-container"]:has(button[kind="primary"]) button[kind="primary"]:hover {
8
+ background-color: #e83277;
9
+ color: white;
10
+ }