borse 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- borse/WORDS.txt +5454 -0
- borse/__about__.py +3 -0
- borse/__init__.py +3 -0
- borse/a1z26.py +41 -0
- borse/braille.py +106 -0
- borse/config.py +130 -0
- borse/game.py +472 -0
- borse/main.py +27 -0
- borse/morse.py +87 -0
- borse/progress.py +167 -0
- borse/semaphore.py +154 -0
- borse/words.py +44 -0
- borse-0.1.0.dist-info/METADATA +141 -0
- borse-0.1.0.dist-info/RECORD +17 -0
- borse-0.1.0.dist-info/WHEEL +4 -0
- borse-0.1.0.dist-info/entry_points.txt +2 -0
- borse-0.1.0.dist-info/licenses/LICENSE +21 -0
borse/game.py
ADDED
|
@@ -0,0 +1,472 @@
|
|
|
1
|
+
"""Curses-based game UI for Borse."""
|
|
2
|
+
|
|
3
|
+
import contextlib
|
|
4
|
+
import curses
|
|
5
|
+
from collections.abc import Callable
|
|
6
|
+
from enum import Enum
|
|
7
|
+
|
|
8
|
+
from borse import a1z26, braille, morse, semaphore
|
|
9
|
+
from borse.config import load_config, save_config
|
|
10
|
+
from borse.progress import load_progress, save_progress
|
|
11
|
+
from borse.words import get_random_word_or_letter
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class GameMode(Enum):
|
|
15
|
+
"""Game mode enumeration."""
|
|
16
|
+
|
|
17
|
+
BRAILLE = "braille"
|
|
18
|
+
MORSE = "morse"
|
|
19
|
+
SEMAPHORE = "semaphore"
|
|
20
|
+
A1Z26 = "a1z26"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class SettingsMode(Enum):
|
|
24
|
+
SETTINGS = "settings"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
# Map modes to their display functions
|
|
28
|
+
MODE_DISPLAY_FUNCS: dict[GameMode, Callable[[str], list[str]]] = {
|
|
29
|
+
GameMode.BRAILLE: braille.get_display_lines,
|
|
30
|
+
GameMode.MORSE: morse.get_display_lines,
|
|
31
|
+
GameMode.SEMAPHORE: semaphore.get_display_lines,
|
|
32
|
+
GameMode.A1Z26: a1z26.get_display_lines,
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
MODE_NAMES: dict[GameMode, str] = {
|
|
36
|
+
GameMode.BRAILLE: "Braille",
|
|
37
|
+
GameMode.MORSE: "Morse Code",
|
|
38
|
+
GameMode.SEMAPHORE: "Semaphore",
|
|
39
|
+
GameMode.A1Z26: "A1Z26",
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
# Keyboard shortcuts for modes
|
|
43
|
+
MODE_SHORTCUTS: dict[str, GameMode] = {
|
|
44
|
+
"b": GameMode.BRAILLE,
|
|
45
|
+
"B": GameMode.BRAILLE,
|
|
46
|
+
"m": GameMode.MORSE,
|
|
47
|
+
"M": GameMode.MORSE,
|
|
48
|
+
"s": GameMode.SEMAPHORE,
|
|
49
|
+
"S": GameMode.SEMAPHORE,
|
|
50
|
+
"a": GameMode.A1Z26,
|
|
51
|
+
"A": GameMode.A1Z26,
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class Game:
|
|
56
|
+
"""Main game class handling the curses UI."""
|
|
57
|
+
|
|
58
|
+
def __init__(self, stdscr: curses.window) -> None:
|
|
59
|
+
"""Initialize the game.
|
|
60
|
+
|
|
61
|
+
Args:
|
|
62
|
+
stdscr: The curses standard screen window.
|
|
63
|
+
"""
|
|
64
|
+
self.stdscr = stdscr
|
|
65
|
+
self.config = load_config()
|
|
66
|
+
self.progress = load_progress(self.config.progress_file)
|
|
67
|
+
|
|
68
|
+
# Setup curses
|
|
69
|
+
curses.curs_set(1) # Show cursor
|
|
70
|
+
curses.use_default_colors()
|
|
71
|
+
self.stdscr.keypad(True)
|
|
72
|
+
|
|
73
|
+
# Make ESC key respond instantly (reduce delay from 1000ms to 25ms)
|
|
74
|
+
curses.set_escdelay(25)
|
|
75
|
+
|
|
76
|
+
# Initialize color pairs if available
|
|
77
|
+
if curses.has_colors():
|
|
78
|
+
curses.start_color()
|
|
79
|
+
curses.init_pair(1, curses.COLOR_GREEN, -1) # Correct
|
|
80
|
+
curses.init_pair(2, curses.COLOR_YELLOW, -1) # Title
|
|
81
|
+
curses.init_pair(3, curses.COLOR_CYAN, -1) # Info
|
|
82
|
+
|
|
83
|
+
def draw_title(self, title: str) -> int:
|
|
84
|
+
"""Draw a title at the top of the screen (left-aligned).
|
|
85
|
+
|
|
86
|
+
Args:
|
|
87
|
+
title: The title text.
|
|
88
|
+
|
|
89
|
+
Returns:
|
|
90
|
+
The next available row.
|
|
91
|
+
"""
|
|
92
|
+
self.stdscr.clear()
|
|
93
|
+
try:
|
|
94
|
+
if curses.has_colors():
|
|
95
|
+
self.stdscr.attron(curses.color_pair(2) | curses.A_BOLD)
|
|
96
|
+
self.stdscr.addstr(1, 2, title)
|
|
97
|
+
if curses.has_colors():
|
|
98
|
+
self.stdscr.attroff(curses.color_pair(2) | curses.A_BOLD)
|
|
99
|
+
except curses.error:
|
|
100
|
+
pass
|
|
101
|
+
return 3
|
|
102
|
+
|
|
103
|
+
def show_menu(self) -> GameMode | SettingsMode | None:
|
|
104
|
+
"""Show the main menu and get mode selection.
|
|
105
|
+
|
|
106
|
+
Returns:
|
|
107
|
+
Selected GameMode, "settings" for settings menu, or None to quit.
|
|
108
|
+
"""
|
|
109
|
+
selected = 0
|
|
110
|
+
modes = list(GameMode)
|
|
111
|
+
# Menu items with keyboard shortcuts shown
|
|
112
|
+
menu_items = [
|
|
113
|
+
"[B] Braille",
|
|
114
|
+
"[M] Morse Code",
|
|
115
|
+
"[S] Semaphore",
|
|
116
|
+
"[A] A1Z26",
|
|
117
|
+
"[O] Options",
|
|
118
|
+
"[Q] Quit",
|
|
119
|
+
]
|
|
120
|
+
|
|
121
|
+
while True:
|
|
122
|
+
row = self.draw_title("BORSE - Code Practice Game")
|
|
123
|
+
height, _ = self.stdscr.getmaxyx()
|
|
124
|
+
|
|
125
|
+
# Show today's progress
|
|
126
|
+
today = self.progress.get_today()
|
|
127
|
+
progress_text = (
|
|
128
|
+
f"Today: {today.total_words} words "
|
|
129
|
+
f"(B:{today.braille_words} M:{today.morse_words} "
|
|
130
|
+
f"S:{today.semaphore_words} A:{today.a1z26_words})"
|
|
131
|
+
)
|
|
132
|
+
try:
|
|
133
|
+
if curses.has_colors():
|
|
134
|
+
self.stdscr.attron(curses.color_pair(3))
|
|
135
|
+
self.stdscr.addstr(row, 2, progress_text)
|
|
136
|
+
if curses.has_colors():
|
|
137
|
+
self.stdscr.attroff(curses.color_pair(3))
|
|
138
|
+
except curses.error:
|
|
139
|
+
pass
|
|
140
|
+
|
|
141
|
+
row += 2
|
|
142
|
+
|
|
143
|
+
# Instructions
|
|
144
|
+
with contextlib.suppress(curses.error):
|
|
145
|
+
self.stdscr.addstr(row, 2, "Select a mode to practice:")
|
|
146
|
+
row += 2
|
|
147
|
+
|
|
148
|
+
# Menu items
|
|
149
|
+
for i, item in enumerate(menu_items):
|
|
150
|
+
try:
|
|
151
|
+
if i == selected:
|
|
152
|
+
self.stdscr.attron(curses.A_REVERSE)
|
|
153
|
+
self.stdscr.addstr(row + i, 4, f" {item} ")
|
|
154
|
+
if i == selected:
|
|
155
|
+
self.stdscr.attroff(curses.A_REVERSE)
|
|
156
|
+
except curses.error:
|
|
157
|
+
pass
|
|
158
|
+
|
|
159
|
+
# Navigation hints
|
|
160
|
+
with contextlib.suppress(curses.error):
|
|
161
|
+
hint_row = min(row + len(menu_items) + 2, height - 2)
|
|
162
|
+
self.stdscr.addstr(
|
|
163
|
+
hint_row, 2, "Use arrows + Enter, or press shortcut key."
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
self.stdscr.refresh()
|
|
167
|
+
|
|
168
|
+
key = self.stdscr.getch()
|
|
169
|
+
|
|
170
|
+
if key == curses.KEY_UP:
|
|
171
|
+
selected = (selected - 1) % len(menu_items)
|
|
172
|
+
elif key == curses.KEY_DOWN:
|
|
173
|
+
selected = (selected + 1) % len(menu_items)
|
|
174
|
+
elif key in (curses.KEY_ENTER, 10, 13):
|
|
175
|
+
if selected < len(modes):
|
|
176
|
+
return modes[selected]
|
|
177
|
+
elif selected == len(modes): # Settings
|
|
178
|
+
return SettingsMode.SETTINGS
|
|
179
|
+
return None # Quit
|
|
180
|
+
elif key == ord("q") or key == ord("Q"):
|
|
181
|
+
return None
|
|
182
|
+
elif key == ord("o") or key == ord("O"):
|
|
183
|
+
return SettingsMode.SETTINGS
|
|
184
|
+
else:
|
|
185
|
+
# Check for shortcut keys
|
|
186
|
+
try:
|
|
187
|
+
char = chr(key)
|
|
188
|
+
if char in MODE_SHORTCUTS:
|
|
189
|
+
return MODE_SHORTCUTS[char]
|
|
190
|
+
except (ValueError, OverflowError):
|
|
191
|
+
pass
|
|
192
|
+
|
|
193
|
+
def play_game(self, mode: GameMode) -> None:
|
|
194
|
+
"""Play a game session in the given mode.
|
|
195
|
+
|
|
196
|
+
Args:
|
|
197
|
+
mode: The game mode to play.
|
|
198
|
+
"""
|
|
199
|
+
display_func = MODE_DISPLAY_FUNCS[mode]
|
|
200
|
+
mode_name = MODE_NAMES[mode]
|
|
201
|
+
words_completed = 0
|
|
202
|
+
total_words = self.config.words_per_game
|
|
203
|
+
completed_words: list[str] = [] # Track completed words
|
|
204
|
+
|
|
205
|
+
while words_completed < total_words:
|
|
206
|
+
word = get_random_word_or_letter(self.config.single_letter_probability)
|
|
207
|
+
user_input = ""
|
|
208
|
+
|
|
209
|
+
while True:
|
|
210
|
+
row = self.draw_title(
|
|
211
|
+
f"{mode_name} - Word {words_completed + 1}/{total_words}"
|
|
212
|
+
)
|
|
213
|
+
height, _width = self.stdscr.getmaxyx()
|
|
214
|
+
|
|
215
|
+
# Display the encoded word
|
|
216
|
+
display_lines = display_func(word)
|
|
217
|
+
for i, line in enumerate(display_lines):
|
|
218
|
+
with contextlib.suppress(curses.error):
|
|
219
|
+
self.stdscr.addstr(row + i, 4, line)
|
|
220
|
+
|
|
221
|
+
row += len(display_lines) + 2
|
|
222
|
+
|
|
223
|
+
# Input prompt - show user input in UPPERCASE
|
|
224
|
+
input_row = row
|
|
225
|
+
input_start = 17
|
|
226
|
+
try:
|
|
227
|
+
self.stdscr.addstr(row, 2, "Type the word: ")
|
|
228
|
+
display_input = user_input.upper()
|
|
229
|
+
self.stdscr.addstr(row, input_start, display_input)
|
|
230
|
+
|
|
231
|
+
# Show correct characters in green
|
|
232
|
+
for i, char in enumerate(user_input):
|
|
233
|
+
if i < len(word) and char.lower() == word[i].lower():
|
|
234
|
+
if curses.has_colors():
|
|
235
|
+
self.stdscr.attron(curses.color_pair(1))
|
|
236
|
+
self.stdscr.addstr(row, input_start + i, char.upper())
|
|
237
|
+
if curses.has_colors():
|
|
238
|
+
self.stdscr.attroff(curses.color_pair(1))
|
|
239
|
+
except curses.error:
|
|
240
|
+
pass
|
|
241
|
+
|
|
242
|
+
row += 2
|
|
243
|
+
|
|
244
|
+
# Instructions
|
|
245
|
+
with contextlib.suppress(curses.error):
|
|
246
|
+
self.stdscr.addstr(row, 2, "Press Esc to return to menu")
|
|
247
|
+
|
|
248
|
+
row += 2
|
|
249
|
+
|
|
250
|
+
# Show completed words in green at the bottom
|
|
251
|
+
if completed_words:
|
|
252
|
+
try:
|
|
253
|
+
# Calculate available space
|
|
254
|
+
available_rows = height - row - 1
|
|
255
|
+
if available_rows > 0:
|
|
256
|
+
if curses.has_colors():
|
|
257
|
+
self.stdscr.attron(curses.color_pair(1))
|
|
258
|
+
self.stdscr.addstr(row, 2, "Completed: ")
|
|
259
|
+
if curses.has_colors():
|
|
260
|
+
self.stdscr.attroff(curses.color_pair(1))
|
|
261
|
+
|
|
262
|
+
# Join words with commas, fitting on available lines
|
|
263
|
+
words_str = ", ".join(w.upper() for w in completed_words)
|
|
264
|
+
if curses.has_colors():
|
|
265
|
+
self.stdscr.attron(curses.color_pair(1))
|
|
266
|
+
self.stdscr.addstr(row, 13, words_str[: _width - 15])
|
|
267
|
+
if curses.has_colors():
|
|
268
|
+
self.stdscr.attroff(curses.color_pair(1))
|
|
269
|
+
except curses.error:
|
|
270
|
+
pass
|
|
271
|
+
|
|
272
|
+
# Position cursor at the typing location
|
|
273
|
+
with contextlib.suppress(curses.error):
|
|
274
|
+
self.stdscr.move(input_row, input_start + len(user_input))
|
|
275
|
+
|
|
276
|
+
self.stdscr.refresh()
|
|
277
|
+
|
|
278
|
+
key = self.stdscr.getch()
|
|
279
|
+
|
|
280
|
+
if key == 27: # Escape
|
|
281
|
+
return
|
|
282
|
+
elif key in (curses.KEY_BACKSPACE, 127, 8):
|
|
283
|
+
user_input = user_input[:-1]
|
|
284
|
+
elif 32 <= key <= 126: # Printable characters
|
|
285
|
+
user_input += chr(key)
|
|
286
|
+
|
|
287
|
+
# Check if word matches
|
|
288
|
+
if user_input.lower() == word.lower():
|
|
289
|
+
words_completed += 1
|
|
290
|
+
completed_words.append(word)
|
|
291
|
+
self.progress.add_word(mode.value)
|
|
292
|
+
save_progress(self.progress, self.config.progress_file)
|
|
293
|
+
break
|
|
294
|
+
|
|
295
|
+
# Show completion message
|
|
296
|
+
self.show_completion(mode, words_completed, completed_words)
|
|
297
|
+
|
|
298
|
+
def show_settings(self) -> None:
|
|
299
|
+
"""Show the settings menu for editing configuration."""
|
|
300
|
+
selected = 0
|
|
301
|
+
settings_items = [
|
|
302
|
+
"words_per_game",
|
|
303
|
+
"single_letter_probability",
|
|
304
|
+
]
|
|
305
|
+
editing: int | None = None
|
|
306
|
+
edit_buffer = ""
|
|
307
|
+
|
|
308
|
+
while True:
|
|
309
|
+
row = self.draw_title("Settings")
|
|
310
|
+
|
|
311
|
+
# Instructions
|
|
312
|
+
with contextlib.suppress(curses.error):
|
|
313
|
+
self.stdscr.addstr(row, 2, "Edit game settings:")
|
|
314
|
+
row += 2
|
|
315
|
+
|
|
316
|
+
# Setting items
|
|
317
|
+
for i, setting in enumerate(settings_items):
|
|
318
|
+
try:
|
|
319
|
+
value = getattr(self.config, setting)
|
|
320
|
+
if setting == "single_letter_probability":
|
|
321
|
+
display_value = f"{value:.0%}"
|
|
322
|
+
else:
|
|
323
|
+
display_value = str(value)
|
|
324
|
+
|
|
325
|
+
if editing == i:
|
|
326
|
+
# Show edit mode
|
|
327
|
+
label = f" {setting}: "
|
|
328
|
+
self.stdscr.addstr(row + i, 4, label)
|
|
329
|
+
self.stdscr.attron(curses.A_UNDERLINE)
|
|
330
|
+
self.stdscr.addstr(row + i, 4 + len(label), edit_buffer + "_")
|
|
331
|
+
self.stdscr.attroff(curses.A_UNDERLINE)
|
|
332
|
+
else:
|
|
333
|
+
label = f" {setting}: {display_value} "
|
|
334
|
+
if i == selected:
|
|
335
|
+
self.stdscr.attron(curses.A_REVERSE)
|
|
336
|
+
self.stdscr.addstr(row + i, 4, label)
|
|
337
|
+
if i == selected:
|
|
338
|
+
self.stdscr.attroff(curses.A_REVERSE)
|
|
339
|
+
except curses.error:
|
|
340
|
+
pass
|
|
341
|
+
|
|
342
|
+
row += len(settings_items) + 2
|
|
343
|
+
|
|
344
|
+
# Navigation hints
|
|
345
|
+
with contextlib.suppress(curses.error):
|
|
346
|
+
if editing is not None:
|
|
347
|
+
self.stdscr.addstr(row, 2, "Enter: save, Esc: cancel")
|
|
348
|
+
else:
|
|
349
|
+
self.stdscr.addstr(row, 2, "Enter: edit, Esc: back to menu")
|
|
350
|
+
|
|
351
|
+
self.stdscr.refresh()
|
|
352
|
+
|
|
353
|
+
key = self.stdscr.getch()
|
|
354
|
+
|
|
355
|
+
if editing is not None:
|
|
356
|
+
# Edit mode
|
|
357
|
+
if key == 27: # Escape - cancel edit
|
|
358
|
+
editing = None
|
|
359
|
+
edit_buffer = ""
|
|
360
|
+
elif key in (curses.KEY_ENTER, 10, 13): # Enter - save
|
|
361
|
+
try:
|
|
362
|
+
setting = settings_items[editing]
|
|
363
|
+
if setting == "words_per_game":
|
|
364
|
+
new_value = int(edit_buffer)
|
|
365
|
+
if new_value > 0:
|
|
366
|
+
self.config.words_per_game = new_value
|
|
367
|
+
elif setting == "single_letter_probability":
|
|
368
|
+
# Accept both decimal (0.3) and percentage (30)
|
|
369
|
+
val = float(edit_buffer)
|
|
370
|
+
if val > 1:
|
|
371
|
+
val = val / 100
|
|
372
|
+
if 0 <= val <= 1:
|
|
373
|
+
self.config.single_letter_probability = val
|
|
374
|
+
save_config(self.config)
|
|
375
|
+
except ValueError:
|
|
376
|
+
pass # Invalid input, ignore
|
|
377
|
+
editing = None
|
|
378
|
+
edit_buffer = ""
|
|
379
|
+
elif key in (curses.KEY_BACKSPACE, 127, 8):
|
|
380
|
+
edit_buffer = edit_buffer[:-1]
|
|
381
|
+
elif 32 <= key <= 126: # Printable characters
|
|
382
|
+
edit_buffer += chr(key)
|
|
383
|
+
else:
|
|
384
|
+
# Navigation mode
|
|
385
|
+
if key == 27: # Escape - back to menu
|
|
386
|
+
return
|
|
387
|
+
elif key == curses.KEY_UP:
|
|
388
|
+
selected = (selected - 1) % len(settings_items)
|
|
389
|
+
elif key == curses.KEY_DOWN:
|
|
390
|
+
selected = (selected + 1) % len(settings_items)
|
|
391
|
+
elif key in (curses.KEY_ENTER, 10, 13):
|
|
392
|
+
editing = selected
|
|
393
|
+
# Pre-fill with current value
|
|
394
|
+
setting = settings_items[selected]
|
|
395
|
+
value = getattr(self.config, setting)
|
|
396
|
+
if setting == "single_letter_probability":
|
|
397
|
+
edit_buffer = str(int(value * 100))
|
|
398
|
+
else:
|
|
399
|
+
edit_buffer = str(value)
|
|
400
|
+
|
|
401
|
+
def show_completion(
|
|
402
|
+
self, mode: GameMode, words_completed: int, completed_words: list[str]
|
|
403
|
+
) -> None:
|
|
404
|
+
"""Show completion screen.
|
|
405
|
+
|
|
406
|
+
Args:
|
|
407
|
+
mode: The game mode that was played.
|
|
408
|
+
words_completed: Number of words completed.
|
|
409
|
+
completed_words: List of completed words.
|
|
410
|
+
"""
|
|
411
|
+
row = self.draw_title("Session Complete!")
|
|
412
|
+
height, width = self.stdscr.getmaxyx()
|
|
413
|
+
|
|
414
|
+
try:
|
|
415
|
+
if curses.has_colors():
|
|
416
|
+
self.stdscr.attron(curses.color_pair(1))
|
|
417
|
+
self.stdscr.addstr(
|
|
418
|
+
row, 2, f"You completed {words_completed} {MODE_NAMES[mode]} words!"
|
|
419
|
+
)
|
|
420
|
+
if curses.has_colors():
|
|
421
|
+
self.stdscr.attroff(curses.color_pair(1))
|
|
422
|
+
|
|
423
|
+
row += 2
|
|
424
|
+
today = self.progress.get_today()
|
|
425
|
+
self.stdscr.addstr(row, 2, f"Today's total: {today.total_words} words")
|
|
426
|
+
|
|
427
|
+
row += 2
|
|
428
|
+
|
|
429
|
+
# Show all completed words
|
|
430
|
+
if completed_words:
|
|
431
|
+
self.stdscr.addstr(row, 2, "Words completed:")
|
|
432
|
+
row += 1
|
|
433
|
+
if curses.has_colors():
|
|
434
|
+
self.stdscr.attron(curses.color_pair(1))
|
|
435
|
+
words_str = ", ".join(w.upper() for w in completed_words)
|
|
436
|
+
# Wrap if needed
|
|
437
|
+
max_width = width - 4
|
|
438
|
+
for i in range(0, len(words_str), max_width):
|
|
439
|
+
if row < height - 3:
|
|
440
|
+
self.stdscr.addstr(row, 2, words_str[i : i + max_width])
|
|
441
|
+
row += 1
|
|
442
|
+
if curses.has_colors():
|
|
443
|
+
self.stdscr.attroff(curses.color_pair(1))
|
|
444
|
+
|
|
445
|
+
row = max(row + 1, height - 3)
|
|
446
|
+
self.stdscr.addstr(min(row, height - 2), 2, "Press any key to continue...")
|
|
447
|
+
except curses.error:
|
|
448
|
+
pass
|
|
449
|
+
|
|
450
|
+
self.stdscr.refresh()
|
|
451
|
+
self.stdscr.getch()
|
|
452
|
+
|
|
453
|
+
def run(self) -> None:
|
|
454
|
+
"""Run the main game loop."""
|
|
455
|
+
while True:
|
|
456
|
+
result = self.show_menu()
|
|
457
|
+
if result is None:
|
|
458
|
+
break
|
|
459
|
+
elif isinstance(result, SettingsMode):
|
|
460
|
+
self.show_settings()
|
|
461
|
+
else:
|
|
462
|
+
self.play_game(result)
|
|
463
|
+
|
|
464
|
+
|
|
465
|
+
def run_game(stdscr: curses.window) -> None:
|
|
466
|
+
"""Entry point for curses wrapper.
|
|
467
|
+
|
|
468
|
+
Args:
|
|
469
|
+
stdscr: The curses standard screen window.
|
|
470
|
+
"""
|
|
471
|
+
game = Game(stdscr)
|
|
472
|
+
game.run()
|
borse/main.py
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""Main entry point for Borse."""
|
|
2
|
+
|
|
3
|
+
import curses
|
|
4
|
+
import sys
|
|
5
|
+
|
|
6
|
+
from borse.game import run_game
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def main() -> int:
|
|
10
|
+
"""Run the Borse game.
|
|
11
|
+
|
|
12
|
+
Returns:
|
|
13
|
+
Exit code (0 for success).
|
|
14
|
+
"""
|
|
15
|
+
try:
|
|
16
|
+
curses.wrapper(run_game)
|
|
17
|
+
return 0
|
|
18
|
+
except KeyboardInterrupt:
|
|
19
|
+
return 0
|
|
20
|
+
except curses.error as e:
|
|
21
|
+
print(f"Terminal error: {e}", file=sys.stderr)
|
|
22
|
+
print("Please ensure your terminal supports curses.", file=sys.stderr)
|
|
23
|
+
return 1
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
if __name__ == "__main__":
|
|
27
|
+
sys.exit(main())
|
borse/morse.py
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
"""Morse code encoding module."""
|
|
2
|
+
|
|
3
|
+
# Morse code mapping for letters and numbers
|
|
4
|
+
MORSE_CODE: dict[str, str] = {
|
|
5
|
+
"A": ".-",
|
|
6
|
+
"B": "-...",
|
|
7
|
+
"C": "-.-.",
|
|
8
|
+
"D": "-..",
|
|
9
|
+
"E": ".",
|
|
10
|
+
"F": "..-.",
|
|
11
|
+
"G": "--.",
|
|
12
|
+
"H": "....",
|
|
13
|
+
"I": "..",
|
|
14
|
+
"J": ".---",
|
|
15
|
+
"K": "-.-",
|
|
16
|
+
"L": ".-..",
|
|
17
|
+
"M": "--",
|
|
18
|
+
"N": "-.",
|
|
19
|
+
"O": "---",
|
|
20
|
+
"P": ".--.",
|
|
21
|
+
"Q": "--.-",
|
|
22
|
+
"R": ".-.",
|
|
23
|
+
"S": "...",
|
|
24
|
+
"T": "-",
|
|
25
|
+
"U": "..-",
|
|
26
|
+
"V": "...-",
|
|
27
|
+
"W": ".--",
|
|
28
|
+
"X": "-..-",
|
|
29
|
+
"Y": "-.--",
|
|
30
|
+
"Z": "--..",
|
|
31
|
+
"0": "-----",
|
|
32
|
+
"1": ".----",
|
|
33
|
+
"2": "..---",
|
|
34
|
+
"3": "...--",
|
|
35
|
+
"4": "....-",
|
|
36
|
+
"5": ".....",
|
|
37
|
+
"6": "-....",
|
|
38
|
+
"7": "--...",
|
|
39
|
+
"8": "---..",
|
|
40
|
+
"9": "----.",
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
# Use Unicode dot and dash for nicer display
|
|
44
|
+
DOT = "●"
|
|
45
|
+
DASH = "━"
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def encode_char(char: str) -> str:
|
|
49
|
+
"""Encode a single character to Morse code.
|
|
50
|
+
|
|
51
|
+
Args:
|
|
52
|
+
char: A single character to encode.
|
|
53
|
+
|
|
54
|
+
Returns:
|
|
55
|
+
The Morse code representation using ● for dot and ━ for dash.
|
|
56
|
+
"""
|
|
57
|
+
upper = char.upper()
|
|
58
|
+
if upper not in MORSE_CODE:
|
|
59
|
+
return ""
|
|
60
|
+
morse = MORSE_CODE[upper]
|
|
61
|
+
# Add space between each dot/dash within the letter
|
|
62
|
+
symbols = [DOT if c == "." else DASH for c in morse]
|
|
63
|
+
return " ".join(symbols)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def encode_word(word: str) -> str:
|
|
67
|
+
"""Encode a word to Morse code.
|
|
68
|
+
|
|
69
|
+
Args:
|
|
70
|
+
word: The word to encode.
|
|
71
|
+
|
|
72
|
+
Returns:
|
|
73
|
+
The Morse code representation with spaces between letters.
|
|
74
|
+
"""
|
|
75
|
+
return " ".join(encode_char(c) for c in word if c.upper() in MORSE_CODE)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def get_display_lines(word: str) -> list[str]:
|
|
79
|
+
"""Get the display lines for a word in Morse code.
|
|
80
|
+
|
|
81
|
+
Args:
|
|
82
|
+
word: The word to encode.
|
|
83
|
+
|
|
84
|
+
Returns:
|
|
85
|
+
A list containing a single line with the Morse code.
|
|
86
|
+
"""
|
|
87
|
+
return [encode_word(word)]
|