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/progress.py
ADDED
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
"""Progress tracking for Borse."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from dataclasses import dataclass, field
|
|
7
|
+
from datetime import date
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclass
|
|
12
|
+
class DailyProgress:
|
|
13
|
+
"""Progress for a single day.
|
|
14
|
+
|
|
15
|
+
Attributes:
|
|
16
|
+
morse_words: Number of Morse code words answered.
|
|
17
|
+
braille_words: Number of Braille words answered.
|
|
18
|
+
semaphore_words: Number of semaphore words answered.
|
|
19
|
+
a1z26_words: Number of A1Z26 words answered.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
morse_words: int = 0
|
|
23
|
+
braille_words: int = 0
|
|
24
|
+
semaphore_words: int = 0
|
|
25
|
+
a1z26_words: int = 0
|
|
26
|
+
|
|
27
|
+
@property
|
|
28
|
+
def total_words(self) -> int:
|
|
29
|
+
"""Get total words answered today.
|
|
30
|
+
|
|
31
|
+
Returns:
|
|
32
|
+
Sum of all words across all modes.
|
|
33
|
+
"""
|
|
34
|
+
return (
|
|
35
|
+
self.morse_words
|
|
36
|
+
+ self.braille_words
|
|
37
|
+
+ self.semaphore_words
|
|
38
|
+
+ self.a1z26_words
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
def to_dict(self) -> dict[str, int]:
|
|
42
|
+
"""Convert to dictionary.
|
|
43
|
+
|
|
44
|
+
Returns:
|
|
45
|
+
Dictionary representation.
|
|
46
|
+
"""
|
|
47
|
+
return {
|
|
48
|
+
"morse_words": self.morse_words,
|
|
49
|
+
"braille_words": self.braille_words,
|
|
50
|
+
"semaphore_words": self.semaphore_words,
|
|
51
|
+
"a1z26_words": self.a1z26_words,
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
@classmethod
|
|
55
|
+
def from_dict(cls, data: dict[str, int]) -> DailyProgress:
|
|
56
|
+
"""Create from dictionary.
|
|
57
|
+
|
|
58
|
+
Args:
|
|
59
|
+
data: Dictionary with progress values.
|
|
60
|
+
|
|
61
|
+
Returns:
|
|
62
|
+
DailyProgress instance.
|
|
63
|
+
"""
|
|
64
|
+
return cls(
|
|
65
|
+
morse_words=data.get("morse_words", 0),
|
|
66
|
+
braille_words=data.get("braille_words", 0),
|
|
67
|
+
semaphore_words=data.get("semaphore_words", 0),
|
|
68
|
+
a1z26_words=data.get("a1z26_words", 0),
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
@dataclass
|
|
73
|
+
class Progress:
|
|
74
|
+
"""Overall progress tracking.
|
|
75
|
+
|
|
76
|
+
Attributes:
|
|
77
|
+
daily: Dictionary mapping date strings to daily progress.
|
|
78
|
+
"""
|
|
79
|
+
|
|
80
|
+
daily: dict[str, DailyProgress] = field(default_factory=dict)
|
|
81
|
+
|
|
82
|
+
def get_today(self) -> DailyProgress:
|
|
83
|
+
"""Get today's progress.
|
|
84
|
+
|
|
85
|
+
Returns:
|
|
86
|
+
DailyProgress for today, creating if needed.
|
|
87
|
+
"""
|
|
88
|
+
today = date.today().isoformat()
|
|
89
|
+
if today not in self.daily:
|
|
90
|
+
self.daily[today] = DailyProgress()
|
|
91
|
+
return self.daily[today]
|
|
92
|
+
|
|
93
|
+
def add_word(self, mode: str) -> None:
|
|
94
|
+
"""Add a completed word for today.
|
|
95
|
+
|
|
96
|
+
Args:
|
|
97
|
+
mode: The game mode ('morse', 'braille', 'semaphore', or 'a1z26').
|
|
98
|
+
"""
|
|
99
|
+
today = self.get_today()
|
|
100
|
+
if mode == "morse":
|
|
101
|
+
today.morse_words += 1
|
|
102
|
+
elif mode == "braille":
|
|
103
|
+
today.braille_words += 1
|
|
104
|
+
elif mode == "semaphore":
|
|
105
|
+
today.semaphore_words += 1
|
|
106
|
+
elif mode == "a1z26":
|
|
107
|
+
today.a1z26_words += 1
|
|
108
|
+
|
|
109
|
+
def to_dict(self) -> dict[str, dict[str, dict[str, int]]]:
|
|
110
|
+
"""Convert to dictionary.
|
|
111
|
+
|
|
112
|
+
Returns:
|
|
113
|
+
Dictionary representation.
|
|
114
|
+
"""
|
|
115
|
+
return {"daily": {k: v.to_dict() for k, v in self.daily.items()}}
|
|
116
|
+
|
|
117
|
+
@classmethod
|
|
118
|
+
def from_dict(cls, data: dict[str, dict[str, dict[str, int]]]) -> Progress:
|
|
119
|
+
"""Create from dictionary.
|
|
120
|
+
|
|
121
|
+
Args:
|
|
122
|
+
data: Dictionary with progress values.
|
|
123
|
+
|
|
124
|
+
Returns:
|
|
125
|
+
Progress instance.
|
|
126
|
+
"""
|
|
127
|
+
daily_data = data.get("daily", {})
|
|
128
|
+
daily = {k: DailyProgress.from_dict(v) for k, v in daily_data.items()}
|
|
129
|
+
return cls(daily=daily)
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def load_progress(progress_path: Path | str) -> Progress:
|
|
133
|
+
"""Load progress from file.
|
|
134
|
+
|
|
135
|
+
Args:
|
|
136
|
+
progress_path: Path to progress file.
|
|
137
|
+
|
|
138
|
+
Returns:
|
|
139
|
+
Progress instance with loaded or default values.
|
|
140
|
+
"""
|
|
141
|
+
path = Path(progress_path)
|
|
142
|
+
|
|
143
|
+
if not path.exists():
|
|
144
|
+
return Progress()
|
|
145
|
+
|
|
146
|
+
try:
|
|
147
|
+
with open(path) as f:
|
|
148
|
+
data = json.load(f)
|
|
149
|
+
return Progress.from_dict(data)
|
|
150
|
+
except (json.JSONDecodeError, OSError):
|
|
151
|
+
return Progress()
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def save_progress(progress: Progress, progress_path: Path | str) -> None:
|
|
155
|
+
"""Save progress to file.
|
|
156
|
+
|
|
157
|
+
Args:
|
|
158
|
+
progress: Progress instance to save.
|
|
159
|
+
progress_path: Path to progress file.
|
|
160
|
+
"""
|
|
161
|
+
path = Path(progress_path)
|
|
162
|
+
|
|
163
|
+
# Ensure directory exists
|
|
164
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
165
|
+
|
|
166
|
+
with open(path, "w") as f:
|
|
167
|
+
json.dump(progress.to_dict(), f, indent=2)
|
borse/semaphore.py
ADDED
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
"""Flag semaphore encoding module with ASCII art display."""
|
|
2
|
+
|
|
3
|
+
# Flag positions (like clock positions):
|
|
4
|
+
# 0 = down (6 o'clock)
|
|
5
|
+
# 1 = down-left (about 7:30)
|
|
6
|
+
# 2 = out-left (9 o'clock)
|
|
7
|
+
# 3 = up-left (about 10:30)
|
|
8
|
+
# 4 = up (12 o'clock)
|
|
9
|
+
# 5 = up-right (about 1:30)
|
|
10
|
+
# 6 = out-right (3 o'clock)
|
|
11
|
+
# 7 = down-right (about 4:30)
|
|
12
|
+
|
|
13
|
+
# Semaphore positions for each letter: (left_flag, right_flag)
|
|
14
|
+
# Positions are numbered 0-7 going clockwise from down
|
|
15
|
+
SEMAPHORE_POSITIONS: dict[str, tuple[int, int]] = {
|
|
16
|
+
"A": (0, 1),
|
|
17
|
+
"B": (0, 2),
|
|
18
|
+
"C": (0, 3),
|
|
19
|
+
"D": (0, 4),
|
|
20
|
+
"E": (0, 5),
|
|
21
|
+
"F": (0, 6),
|
|
22
|
+
"G": (0, 7),
|
|
23
|
+
"H": (1, 2),
|
|
24
|
+
"I": (1, 3),
|
|
25
|
+
"J": (4, 6),
|
|
26
|
+
"K": (1, 4),
|
|
27
|
+
"L": (1, 5),
|
|
28
|
+
"M": (1, 6),
|
|
29
|
+
"N": (1, 7),
|
|
30
|
+
"O": (2, 3),
|
|
31
|
+
"P": (2, 4),
|
|
32
|
+
"Q": (2, 5),
|
|
33
|
+
"R": (2, 6),
|
|
34
|
+
"S": (2, 7),
|
|
35
|
+
"T": (3, 4),
|
|
36
|
+
"U": (3, 5),
|
|
37
|
+
"V": (4, 7),
|
|
38
|
+
"W": (5, 6),
|
|
39
|
+
"X": (5, 7),
|
|
40
|
+
"Y": (3, 6),
|
|
41
|
+
"Z": (6, 7),
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
# Grid is 7x5 (wider for horizontal arms):
|
|
45
|
+
# 0 1 2 3 4 5 6
|
|
46
|
+
# 7 8 9 10 11 12 13
|
|
47
|
+
# 14 15 16 17 18 19 20
|
|
48
|
+
# 21 22 23 24 25 26 27
|
|
49
|
+
# 28 29 30 31 32 33 34
|
|
50
|
+
# Position 17 is center (the person)
|
|
51
|
+
|
|
52
|
+
# Grid positions for each flag position
|
|
53
|
+
# Most positions have 2 cells (inner, outer)
|
|
54
|
+
# Horizontal positions (2, 6) have 3 cells for longer arms
|
|
55
|
+
POSITION_TO_GRID: dict[int, tuple[int, ...]] = {
|
|
56
|
+
0: (24, 31), # down
|
|
57
|
+
1: (23, 29), # down-left
|
|
58
|
+
2: (16, 15, 14), # out-left (3 hyphens)
|
|
59
|
+
3: (9, 1), # up-left
|
|
60
|
+
4: (10, 3), # up
|
|
61
|
+
5: (11, 5), # up-right
|
|
62
|
+
6: (18, 19, 20), # out-right (3 hyphens)
|
|
63
|
+
7: (25, 33), # down-right
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
# Characters to show for each flag position
|
|
67
|
+
# Based on the direction from center
|
|
68
|
+
POSITION_CHARS: dict[int, str] = {
|
|
69
|
+
0: "|", # down
|
|
70
|
+
1: "/", # down-left
|
|
71
|
+
2: "-", # out-left
|
|
72
|
+
3: "\\", # up-left
|
|
73
|
+
4: "|", # up
|
|
74
|
+
5: "/", # up-right
|
|
75
|
+
6: "-", # out-right
|
|
76
|
+
7: "\\", # down-right
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def encode_char(char: str) -> list[str]:
|
|
81
|
+
"""Encode a single character to semaphore ASCII art.
|
|
82
|
+
|
|
83
|
+
Args:
|
|
84
|
+
char: A single character to encode.
|
|
85
|
+
|
|
86
|
+
Returns:
|
|
87
|
+
A list of 5 strings representing the 5 rows of the semaphore display.
|
|
88
|
+
"""
|
|
89
|
+
upper = char.upper()
|
|
90
|
+
if upper not in SEMAPHORE_POSITIONS:
|
|
91
|
+
return [" ", " ", " ", " ", " "]
|
|
92
|
+
|
|
93
|
+
left_pos, right_pos = SEMAPHORE_POSITIONS[upper]
|
|
94
|
+
|
|
95
|
+
# Build the 7x5 grid
|
|
96
|
+
grid = [" "] * 35
|
|
97
|
+
grid[17] = "O" # Person in center
|
|
98
|
+
|
|
99
|
+
# Place the flags (variable length based on position)
|
|
100
|
+
left_cells = POSITION_TO_GRID[left_pos]
|
|
101
|
+
right_cells = POSITION_TO_GRID[right_pos]
|
|
102
|
+
|
|
103
|
+
left_char = POSITION_CHARS[left_pos]
|
|
104
|
+
right_char = POSITION_CHARS[right_pos]
|
|
105
|
+
|
|
106
|
+
for cell in left_cells:
|
|
107
|
+
grid[cell] = left_char
|
|
108
|
+
for cell in right_cells:
|
|
109
|
+
grid[cell] = right_char
|
|
110
|
+
|
|
111
|
+
# Convert to 5 rows (7 columns each)
|
|
112
|
+
rows = [
|
|
113
|
+
"".join(grid[0:7]),
|
|
114
|
+
"".join(grid[7:14]),
|
|
115
|
+
"".join(grid[14:21]),
|
|
116
|
+
"".join(grid[21:28]),
|
|
117
|
+
"".join(grid[28:35]),
|
|
118
|
+
]
|
|
119
|
+
|
|
120
|
+
return rows
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def encode_word(word: str) -> list[list[str]]:
|
|
124
|
+
"""Encode a word to semaphore ASCII art.
|
|
125
|
+
|
|
126
|
+
Args:
|
|
127
|
+
word: The word to encode.
|
|
128
|
+
|
|
129
|
+
Returns:
|
|
130
|
+
A list of character encodings, each being a list of 5 row strings.
|
|
131
|
+
"""
|
|
132
|
+
return [encode_char(c) for c in word if c.upper() in SEMAPHORE_POSITIONS]
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def get_display_lines(word: str) -> list[str]:
|
|
136
|
+
"""Get the display lines for a word in semaphore.
|
|
137
|
+
|
|
138
|
+
Args:
|
|
139
|
+
word: The word to encode.
|
|
140
|
+
|
|
141
|
+
Returns:
|
|
142
|
+
A list of 5 strings, one for each row, with characters separated by spaces.
|
|
143
|
+
"""
|
|
144
|
+
chars = encode_word(word)
|
|
145
|
+
if not chars:
|
|
146
|
+
return ["", "", "", "", ""]
|
|
147
|
+
|
|
148
|
+
# Combine all characters horizontally with space between
|
|
149
|
+
lines = []
|
|
150
|
+
for row in range(5):
|
|
151
|
+
line = " ".join(char[row] for char in chars)
|
|
152
|
+
lines.append(line)
|
|
153
|
+
|
|
154
|
+
return lines
|
borse/words.py
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"""Word list for the game."""
|
|
2
|
+
|
|
3
|
+
import random
|
|
4
|
+
import string
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
LETTERS = string.ascii_lowercase
|
|
8
|
+
PATH_TO_WORDS = Path(__file__).with_name("WORDS.txt")
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
with open(PATH_TO_WORDS) as f:
|
|
12
|
+
COMMON_WORDS = [line.strip() for line in f]
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def get_random_word() -> str:
|
|
16
|
+
"""Get a random word from the word list.
|
|
17
|
+
|
|
18
|
+
Returns:
|
|
19
|
+
A random common English word.
|
|
20
|
+
"""
|
|
21
|
+
return random.choice(COMMON_WORDS)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def get_random_letter() -> str:
|
|
25
|
+
"""Get a random single letter from A-Z.
|
|
26
|
+
|
|
27
|
+
Returns:
|
|
28
|
+
A random lowercase letter.
|
|
29
|
+
"""
|
|
30
|
+
return random.choice(LETTERS)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def get_random_word_or_letter(single_letter_probability: float = 0.3) -> str:
|
|
34
|
+
"""Get either a random word or a single letter based on probability.
|
|
35
|
+
|
|
36
|
+
Args:
|
|
37
|
+
single_letter_probability: Probability (0-1) of returning a single letter.
|
|
38
|
+
|
|
39
|
+
Returns:
|
|
40
|
+
A random word or single letter.
|
|
41
|
+
"""
|
|
42
|
+
if random.random() < single_letter_probability:
|
|
43
|
+
return get_random_letter()
|
|
44
|
+
return get_random_word()
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: borse
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A terminal game for practicing Morse code, Braille, and semaphore.
|
|
5
|
+
Project-URL: repository, https://github.com/vEnhance/borse
|
|
6
|
+
Author-email: Evan Chen <evan@evanchen.cc>
|
|
7
|
+
License: MIT
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
Requires-Python: <4.0,>=3.10
|
|
10
|
+
Requires-Dist: tomli-w>=1.0.0
|
|
11
|
+
Requires-Dist: tomli>=2.0.0; python_version < '3.11'
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
|
|
14
|
+
# borse
|
|
15
|
+
|
|
16
|
+
**borse** is a terminal program meant to practice
|
|
17
|
+
reading braille, Morse code, and semaphore,
|
|
18
|
+
which are common encodings for
|
|
19
|
+
[puzzle hunts](https://web.evanchen.cc/upload/EvanPuzzleCodings.pdf).
|
|
20
|
+
Also supports A1Z26 practice.
|
|
21
|
+
|
|
22
|
+
## Usage
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
uvx borse
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Or you can install from PyPI by using `uv`, `pip`, etc.
|
|
29
|
+
|
|
30
|
+
## Configuration
|
|
31
|
+
|
|
32
|
+
Configuration is stored in `~/.config/borse/config.json` by default:
|
|
33
|
+
|
|
34
|
+
```json
|
|
35
|
+
{
|
|
36
|
+
"progress_file": "~/.config/borse/progress.json",
|
|
37
|
+
"words_per_game": 10
|
|
38
|
+
}
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Your daily progress is also automatically saved and displayed on the main menu.
|
|
42
|
+
|
|
43
|
+
## Hints for memorizing the encodings
|
|
44
|
+
|
|
45
|
+
Actually this is a note-to-self.
|
|
46
|
+
|
|
47
|
+
### Remembering braille
|
|
48
|
+
|
|
49
|
+
For Grade 1 (just the letters `A-Z`),
|
|
50
|
+
the chart on [Wikipedia](https://en.wikipedia.org/wiki/English_Braille)
|
|
51
|
+
is helpful!
|
|
52
|
+
The trick is to memorize just the first 10 symbols for `A-J`,
|
|
53
|
+
which only use the upper four dots.
|
|
54
|
+
That's because `K-T` are the same as `A-J` with one extra dot,
|
|
55
|
+
while `UVXYZ` are `A-E` with one extra dot.
|
|
56
|
+
|
|
57
|
+
In real life, Grade 2 braille has some additional contractions.
|
|
58
|
+
It might be nice to add these into borse at some point.
|
|
59
|
+
|
|
60
|
+
### Remembering Morse code
|
|
61
|
+
|
|
62
|
+
In Morse code, the most frequent letters are shorter.
|
|
63
|
+
So I think it's a lot easier to remember Morse code as a binary tree,
|
|
64
|
+
since the common letters will all be towards the top.
|
|
65
|
+
I found [this picture on the Internet](https://slidetodoc.com/binary-trees-binary-tree-structure-root-node-stores/):
|
|
66
|
+
|
|
67
|
+

|
|
68
|
+
|
|
69
|
+
I think the dot looks like 0 and a dash looks like a (rotated) 1,
|
|
70
|
+
so it makes sense to me that dots are in the left of the tree.
|
|
71
|
+
|
|
72
|
+
Then you can just memorize the letters in each row in order.
|
|
73
|
+
Here are some terrible mnemonics I made up that worked for me
|
|
74
|
+
for the first three rows (you're on your own for the last one):
|
|
75
|
+
|
|
76
|
+
- `ET`: Eastern Time, or a [1982 movie][et]
|
|
77
|
+
- `IANM`: I Am Not Mad
|
|
78
|
+
- `SURWDKGO`: [SuperUser][su] [ReWrote][rw] [DynamicKernel][dk] in [GO][go]
|
|
79
|
+
|
|
80
|
+
(Also, `surdwkgo` is also the name of a
|
|
81
|
+
[Taiwanese CodeForces grandmaster](https://codeforces.com/profile/surwdkgo).)
|
|
82
|
+
|
|
83
|
+
[et]: https://en.wikipedia.org/wiki/E.T._the_Extra-Terrestrial
|
|
84
|
+
[su]: https://en.wikipedia.org/wiki/Su_(Unix)
|
|
85
|
+
[rw]: https://lean-lang.org/doc/reference/latest/Tactic-Proofs/Tactic-Reference/#rw
|
|
86
|
+
[dk]: https://en.wikipedia.org/wiki/Dynamic_Kernel_Module_Support
|
|
87
|
+
[go]: https://en.wikipedia.org/wiki/Go_(programming_language)
|
|
88
|
+
|
|
89
|
+
### Remembering semaphore
|
|
90
|
+
|
|
91
|
+
If you look at a semaphore chart,
|
|
92
|
+
what you'll find is that there are some groups of adjacent letters
|
|
93
|
+
that just differ in one hand rotating clockwise.
|
|
94
|
+
For example, the letters from `A-G` are obtained
|
|
95
|
+
by fixing one arm at 6 o'clock and rotating the other arm
|
|
96
|
+
all the way from 7:30 to 4:30.
|
|
97
|
+
|
|
98
|
+
So in my head, I organize the letters in "blocks",
|
|
99
|
+
where each block starts with two arms at a 45-degree angle,
|
|
100
|
+
and then having the other arm rotate clockwise.
|
|
101
|
+
The resulting blocks are then easier to remember:
|
|
102
|
+
|
|
103
|
+
- **A block**: `ABCDEFG`
|
|
104
|
+
- **H block**: `HIKLMN` (note `J` is missing)
|
|
105
|
+
- **O block**: `OPQRS`
|
|
106
|
+
- **T block**: `TUY` (note the additional `Y`)
|
|
107
|
+
- **# block**: `#JV` (this is the weird exceptions one)
|
|
108
|
+
- **W block**: `WX`
|
|
109
|
+
- **Z block**: `Z`
|
|
110
|
+
|
|
111
|
+
I don't know if `A HOT #WZ` means anything to you.
|
|
112
|
+
|
|
113
|
+
## Development
|
|
114
|
+
|
|
115
|
+
Set up by cloning the repository and running
|
|
116
|
+
|
|
117
|
+
```bash
|
|
118
|
+
uv sync
|
|
119
|
+
uv run prek install
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
To manually run the linter and tests
|
|
123
|
+
|
|
124
|
+
```bash
|
|
125
|
+
uv run prek --all-files # run linter
|
|
126
|
+
uv run prek --all-files --hook-stage pre-push
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
## FAQ
|
|
130
|
+
|
|
131
|
+
- _Where does the name come from?_
|
|
132
|
+
|
|
133
|
+
From `Braille mORse SEmaphore`.
|
|
134
|
+
|
|
135
|
+
- _Should "braille" be capitalized?_
|
|
136
|
+
|
|
137
|
+
[No](https://www.brailleauthority.org/capitalization/capitalization.html).
|
|
138
|
+
|
|
139
|
+
- _Why would you spend time learning this?_
|
|
140
|
+
|
|
141
|
+
It could be a great conversation starter for a first date…
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
borse/WORDS.txt,sha256=eEjDg7tmTgtFzhQMQrSNx58UdPpPmnax7JZIHjRGA5g,40575
|
|
2
|
+
borse/__about__.py,sha256=0zET0Hr7ZlMR2QzPnTxlIVERltHMYywiz05ap-993o8,67
|
|
3
|
+
borse/__init__.py,sha256=M5ARPgePzj8zEaqyk11jzt0RtDD5nbhA2KPS31vGJRk,109
|
|
4
|
+
borse/a1z26.py,sha256=BmkTTxeCu4fvjFRMDSZktJWu0E169H32uga1khkl7gY,964
|
|
5
|
+
borse/braille.py,sha256=h1q_r_4Am3_RswL3gqsa5Mm076RU0vSY7TSsQ72ExZQ,2642
|
|
6
|
+
borse/config.py,sha256=eUDNRkxTIQRRpCnm9P3tQgFKjS4f2MF5-lNZFtt78Jg,3695
|
|
7
|
+
borse/game.py,sha256=h_vLIRk-AX0FeHiL9y5UCj-1QyJW2iSOCFFPWjUvOnA,17278
|
|
8
|
+
borse/main.py,sha256=peNdARRdBjtCP0rg2_hjq1GRkESWQhfxxAuGMYxyCkw,535
|
|
9
|
+
borse/morse.py,sha256=nh7ptvXaoIHoUN_eval0-nZRd4oWyDNpDdnxsclx9F0,1818
|
|
10
|
+
borse/progress.py,sha256=CPsv5SMF4ulHKbZu95_qK2_kpR1_BX1OFWChSoiKl44,4390
|
|
11
|
+
borse/semaphore.py,sha256=mdU1siRkW4eAAgtCuSdc7aMCr2-DbZOwgmzpGnism8I,3855
|
|
12
|
+
borse/words.py,sha256=3YifBii9OuMI2eHXAtvENxVM5n0T4eyZTU-NJ7EPmtQ,1023
|
|
13
|
+
borse-0.1.0.dist-info/METADATA,sha256=LRsX8A7DmCJIa6pFCisk_MGg7KDhS3ybCPHea6ASnOg,4175
|
|
14
|
+
borse-0.1.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
15
|
+
borse-0.1.0.dist-info/entry_points.txt,sha256=e3JYLL_H22Xgqe3_TyecetryyyE2kt07nBOGz4T41Ic,42
|
|
16
|
+
borse-0.1.0.dist-info/licenses/LICENSE,sha256=gq-dD45uKs1sNrFCbrHXC8PpsWoSauIPzU-NEQHmTEc,1066
|
|
17
|
+
borse-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Evan Chen
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|