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/__about__.py ADDED
@@ -0,0 +1,3 @@
1
+ __version__ = "0.1.0"
2
+ __author__ = "Evan Chen"
3
+ __license__ = "MIT"
borse/__init__.py ADDED
@@ -0,0 +1,3 @@
1
+ """Borse - A terminal game for practicing Morse code, Braille, and flag semaphore."""
2
+
3
+ __version__ = "0.1.0"
borse/a1z26.py ADDED
@@ -0,0 +1,41 @@
1
+ """A1Z26 encoding module (A=1, B=2, ..., Z=26)."""
2
+
3
+
4
+ def encode_char(char: str) -> str:
5
+ """Encode a single character to A1Z26.
6
+
7
+ Args:
8
+ char: A single character to encode.
9
+
10
+ Returns:
11
+ The number representation (1-26) or empty string for invalid chars.
12
+ """
13
+ upper = char.upper()
14
+ if not upper.isalpha() or len(upper) != 1:
15
+ return ""
16
+ return str(ord(upper) - ord("A") + 1)
17
+
18
+
19
+ def encode_word(word: str) -> str:
20
+ """Encode a word to A1Z26.
21
+
22
+ Args:
23
+ word: The word to encode.
24
+
25
+ Returns:
26
+ The A1Z26 representation with dashes between numbers.
27
+ """
28
+ numbers = [encode_char(c) for c in word if c.isalpha()]
29
+ return " ".join(numbers)
30
+
31
+
32
+ def get_display_lines(word: str) -> list[str]:
33
+ """Get the display lines for a word in A1Z26.
34
+
35
+ Args:
36
+ word: The word to encode.
37
+
38
+ Returns:
39
+ A list containing a single line with the A1Z26 encoding.
40
+ """
41
+ return [encode_word(word)]
borse/braille.py ADDED
@@ -0,0 +1,106 @@
1
+ """Braille encoding module with ASCII art display."""
2
+
3
+ # Braille patterns for letters a-z
4
+ # Dots are numbered:
5
+ # 1 4
6
+ # 2 5
7
+ # 3 6
8
+ # We store which dots are raised for each letter
9
+ BRAILLE_PATTERNS: dict[str, tuple[int, ...]] = {
10
+ "A": (1,),
11
+ "B": (1, 2),
12
+ "C": (1, 4),
13
+ "D": (1, 4, 5),
14
+ "E": (1, 5),
15
+ "F": (1, 2, 4),
16
+ "G": (1, 2, 4, 5),
17
+ "H": (1, 2, 5),
18
+ "I": (2, 4),
19
+ "J": (2, 4, 5),
20
+ "K": (1, 3),
21
+ "L": (1, 2, 3),
22
+ "M": (1, 3, 4),
23
+ "N": (1, 3, 4, 5),
24
+ "O": (1, 3, 5),
25
+ "P": (1, 2, 3, 4),
26
+ "Q": (1, 2, 3, 4, 5),
27
+ "R": (1, 2, 3, 5),
28
+ "S": (2, 3, 4),
29
+ "T": (2, 3, 4, 5),
30
+ "U": (1, 3, 6),
31
+ "V": (1, 2, 3, 6),
32
+ "W": (2, 4, 5, 6),
33
+ "X": (1, 3, 4, 6),
34
+ "Y": (1, 3, 4, 5, 6),
35
+ "Z": (1, 3, 5, 6),
36
+ }
37
+
38
+ # Filled and unfilled circles for display
39
+ FILLED = "●"
40
+ UNFILLED = "○"
41
+
42
+
43
+ def encode_char(char: str) -> list[str]:
44
+ """Encode a single character to Braille ASCII art.
45
+
46
+ Args:
47
+ char: A single character to encode.
48
+
49
+ Returns:
50
+ A list of 3 strings representing the 3 rows of the Braille cell.
51
+ """
52
+ upper = char.upper()
53
+ if upper not in BRAILLE_PATTERNS:
54
+ return [" ", " ", " "]
55
+
56
+ dots = set(BRAILLE_PATTERNS[upper])
57
+
58
+ # Build the 3x2 grid
59
+ # Row 1: dots 1, 4
60
+ # Row 2: dots 2, 5
61
+ # Row 3: dots 3, 6
62
+ rows = []
63
+ for _row_idx, (left_dot, right_dot) in enumerate([(1, 4), (2, 5), (3, 6)], start=1):
64
+ left = FILLED if left_dot in dots else UNFILLED
65
+ right = FILLED if right_dot in dots else UNFILLED
66
+ # Add space between left and right columns
67
+ rows.append(f"{left} {right}")
68
+
69
+ return rows
70
+
71
+
72
+ def encode_word(word: str) -> list[list[str]]:
73
+ """Encode a word to Braille ASCII art.
74
+
75
+ Args:
76
+ word: The word to encode.
77
+
78
+ Returns:
79
+ A list of character encodings, each being a list of 3 row strings.
80
+ """
81
+ return [encode_char(c) for c in word if c.upper() in BRAILLE_PATTERNS]
82
+
83
+
84
+ def get_display_lines(word: str) -> list[str]:
85
+ """Get the display lines for a word in Braille.
86
+
87
+ Args:
88
+ word: The word to encode.
89
+
90
+ Returns:
91
+ A list of 5 strings (3 rows with blank lines between for vertical spacing).
92
+ """
93
+ chars = encode_word(word)
94
+ if not chars:
95
+ return ["", "", "", "", ""]
96
+
97
+ # Combine all characters horizontally with space between
98
+ # Add blank lines between rows for vertical spacing
99
+ lines = []
100
+ for row in range(3):
101
+ line = " ".join(char[row] for char in chars)
102
+ lines.append(line)
103
+ if row < 2: # Add blank line after first two rows
104
+ lines.append("")
105
+
106
+ return lines
borse/config.py ADDED
@@ -0,0 +1,130 @@
1
+ """Configuration management for Borse."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ from dataclasses import dataclass, field
7
+ from pathlib import Path
8
+
9
+ try:
10
+ import tomllib
11
+ except ImportError:
12
+ import tomli as tomllib # type: ignore[import-not-found,no-redef]
13
+
14
+ import tomli_w
15
+
16
+
17
+ def get_default_config_dir() -> Path:
18
+ """Get the default configuration directory following XDG spec.
19
+
20
+ Returns:
21
+ Path to the configuration directory ($XDG_CONFIG_HOME/borse/ or ~/.config/borse/).
22
+ """
23
+ xdg_config_home = os.environ.get("XDG_CONFIG_HOME")
24
+ if xdg_config_home:
25
+ return Path(xdg_config_home) / "borse"
26
+ return Path.home() / ".config" / "borse"
27
+
28
+
29
+ def get_default_config_path() -> Path:
30
+ """Get the default configuration file path.
31
+
32
+ Returns:
33
+ Path to the configuration file (~/.config/borse/config.toml).
34
+ """
35
+ return get_default_config_dir() / "config.toml"
36
+
37
+
38
+ def get_default_progress_path() -> Path:
39
+ """Get the default progress file path.
40
+
41
+ Returns:
42
+ Path to the progress file (~/.config/borse/progress.json).
43
+ """
44
+ return get_default_config_dir() / "progress.json"
45
+
46
+
47
+ @dataclass
48
+ class Config:
49
+ """Configuration settings for Borse.
50
+
51
+ Attributes:
52
+ progress_file: Path to the progress tracking file.
53
+ words_per_game: Number of words to show in each game session.
54
+ single_letter_probability: Probability (0-1) of showing a single letter instead of a word.
55
+ """
56
+
57
+ progress_file: str = field(default_factory=lambda: str(get_default_progress_path()))
58
+ words_per_game: int = 10
59
+ single_letter_probability: float = 0.3
60
+
61
+ def to_dict(self) -> dict[str, str | int | float]:
62
+ """Convert config to dictionary.
63
+
64
+ Returns:
65
+ Dictionary representation of the config.
66
+ """
67
+ return {
68
+ "progress_file": self.progress_file,
69
+ "words_per_game": self.words_per_game,
70
+ "single_letter_probability": self.single_letter_probability,
71
+ }
72
+
73
+ @classmethod
74
+ def from_dict(cls, data: dict[str, str | int | float]) -> Config:
75
+ """Create config from dictionary.
76
+
77
+ Args:
78
+ data: Dictionary with config values.
79
+
80
+ Returns:
81
+ Config instance.
82
+ """
83
+ return cls(
84
+ progress_file=str(
85
+ data.get("progress_file", str(get_default_progress_path()))
86
+ ),
87
+ words_per_game=int(data.get("words_per_game", 10)),
88
+ single_letter_probability=float(data.get("single_letter_probability", 0.3)),
89
+ )
90
+
91
+
92
+ def load_config(config_path: Path | None = None) -> Config:
93
+ """Load configuration from file.
94
+
95
+ Args:
96
+ config_path: Path to config file. Defaults to $XDG_CONFIG_HOME/borse/config.toml.
97
+
98
+ Returns:
99
+ Config instance with loaded or default values.
100
+ """
101
+ if config_path is None:
102
+ config_path = get_default_config_path()
103
+
104
+ if not config_path.exists():
105
+ config = Config()
106
+ return config
107
+
108
+ try:
109
+ with open(config_path, "rb") as f:
110
+ data = tomllib.load(f)
111
+ return Config.from_dict(data)
112
+ except (tomllib.TOMLDecodeError, OSError):
113
+ return Config()
114
+
115
+
116
+ def save_config(config: Config, config_path: Path | None = None) -> None:
117
+ """Save configuration to file.
118
+
119
+ Args:
120
+ config: Config instance to save.
121
+ config_path: Path to config file. Defaults to ~/.config/borse/config.toml.
122
+ """
123
+ if config_path is None:
124
+ config_path = get_default_config_path()
125
+
126
+ # Ensure directory exists
127
+ config_path.parent.mkdir(parents=True, exist_ok=True)
128
+
129
+ with open(config_path, "wb") as f:
130
+ tomli_w.dump(config.to_dict(), f)