libascii 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.
libascii/__init__.py ADDED
@@ -0,0 +1,104 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import TYPE_CHECKING, Literal, overload
4
+
5
+ if TYPE_CHECKING:
6
+ from .chars import CharLib
7
+ from .color import Color
8
+ from .game import Game
9
+ from .grid import Grid
10
+ from .shapes.ellipse import Ellipse
11
+ from .shapes.line import Line
12
+ from .shapes.rectangle import Rectangle
13
+ from .shapes.shape import Shape
14
+ from .shapes.triangle import Triangle
15
+ from .sound import Sound
16
+
17
+
18
+ @overload
19
+ def __getattr__(name: Literal["Color"]) -> type[Color]: ...
20
+
21
+
22
+ @overload
23
+ def __getattr__(name: Literal["Grid"]) -> type[Grid]: ...
24
+
25
+
26
+ @overload
27
+ def __getattr__(name: Literal["CharLib"]) -> CharLib: ...
28
+
29
+
30
+ @overload
31
+ def __getattr__(name: Literal["Game"]) -> type[Game]: ...
32
+
33
+
34
+ @overload
35
+ def __getattr__(name: Literal["Shape"]) -> type[Shape]: ...
36
+
37
+
38
+ @overload
39
+ def __getattr__(name: Literal["Rectangle"]) -> type[Rectangle]: ...
40
+
41
+
42
+ @overload
43
+ def __getattr__(name: Literal["Ellipse"]) -> type[Ellipse]: ...
44
+
45
+
46
+ @overload
47
+ def __getattr__(name: Literal["Triangle"]) -> type[Triangle]: ...
48
+
49
+
50
+ @overload
51
+ def __getattr__(name: Literal["Line"]) -> type[Line]: ...
52
+
53
+
54
+ @overload
55
+ def __getattr__(name: Literal["Sound"]) -> type[Sound]: ...
56
+
57
+
58
+ @overload
59
+ def __getattr__(name: str) -> object: ...
60
+
61
+
62
+ def __getattr__(name: str) -> object:
63
+ match name:
64
+ case "Color":
65
+ from .color import Color
66
+
67
+ return Color
68
+ case "Grid":
69
+ from .grid import Grid
70
+
71
+ return Grid
72
+ case "CharLib":
73
+ from .chars import CharLib
74
+
75
+ return CharLib
76
+ case "Game":
77
+ from .game import Game
78
+
79
+ return Game
80
+ case "Shape":
81
+ from .shapes.shape import Shape
82
+
83
+ return Shape
84
+ case "Rectangle":
85
+ from .shapes.rectangle import Rectangle
86
+
87
+ return Rectangle
88
+ case "Ellipse":
89
+ from .shapes.ellipse import Ellipse
90
+
91
+ return Ellipse
92
+ case "Triangle":
93
+ from .shapes.triangle import Triangle
94
+
95
+ return Triangle
96
+ case "Line":
97
+ from .shapes.line import Line
98
+
99
+ return Line
100
+ case "Sound":
101
+ from .sound import Sound
102
+
103
+ return Sound
104
+ raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
File without changes
@@ -0,0 +1,60 @@
1
+ MODIFIER_MAP = {
2
+ "ctrl": "Control",
3
+ "shift": "Shift",
4
+ "alt": "Alt",
5
+ "meta": "Meta",
6
+ }
7
+
8
+ MODIFIER_ORDER = ("ctrl", "shift", "alt", "meta")
9
+
10
+
11
+ def expand_keybinding(keybinding: str) -> list[tuple[frozenset[str], str]]:
12
+ combinations: list[tuple[frozenset[str], str]] = []
13
+ for pattern in keybinding.split(" or "):
14
+ pattern_combos: list[tuple[frozenset[str], str | None]] = [
15
+ (frozenset(), None)
16
+ ]
17
+
18
+ for token in [token.strip() for token in pattern.split("+")]:
19
+ if token.startswith("[") and token.endswith("]"):
20
+ modifier = token[1:-1]
21
+ expanded: list[tuple[frozenset[str], str | None]] = []
22
+
23
+ for modifiers, key in pattern_combos:
24
+ expanded.append((modifiers, key))
25
+ expanded.append((modifiers | {modifier}, key))
26
+ pattern_combos = expanded
27
+ elif token.startswith("{") and token.endswith("}"):
28
+ choices = [choice.strip() for choice in token[1:-1].split(",")]
29
+ expanded = []
30
+ for modifiers, key in pattern_combos:
31
+ for choice in choices:
32
+ expanded.append((modifiers | {choice}, key))
33
+ pattern_combos = expanded
34
+ elif token in MODIFIER_MAP:
35
+ pattern_combos = [
36
+ (modifiers | {token}, key)
37
+ for modifiers, key in pattern_combos
38
+ ]
39
+ else:
40
+ pattern_combos = [
41
+ (modifiers, token) for modifiers, _ in pattern_combos
42
+ ]
43
+
44
+ for modifiers, key in pattern_combos:
45
+ if key is not None:
46
+ combinations.append((modifiers, key))
47
+ return combinations
48
+
49
+
50
+ def keybinding_to_events(keybinding: str, event_type: str) -> list[str]:
51
+ events: list[str] = []
52
+ for modifiers, key in expand_keybinding(keybinding):
53
+ ordered = [
54
+ MODIFIER_MAP[modifier]
55
+ for modifier in MODIFIER_ORDER
56
+ if modifier in modifiers
57
+ ]
58
+ parts = [*ordered, event_type, key]
59
+ events.append(f"<{'-'.join(parts)}>")
60
+ return events
libascii/chars.py ADDED
@@ -0,0 +1,134 @@
1
+ CharLib: dict[str, dict[str | int, str]] = {
2
+ "MAIN": {
3
+ "BLANK": " ",
4
+ "LIGHT_SHADE": "░",
5
+ "MEDIUM_SHADE": "▒",
6
+ "DARK_SHADE": "▓",
7
+ "BLOCK": "█",
8
+ "SQUARE_BLACK": "■",
9
+ "SQUARE_WHITE": "□",
10
+ "CIRCLE_BLACK": "●",
11
+ "CIRCLE_WHITE": "○",
12
+ "DIAMOND_BLACK": "◆",
13
+ "DIAMOND_WHITE": "◇",
14
+ "TRIANGLE_UP_BLACK": "▲",
15
+ "TRIANGLE_UP_WHITE": "△",
16
+ "TRIANGLE_DOWN_BLACK": "▼",
17
+ "TRIANGLE_DOWN_WHITE": "▽",
18
+ "TRIANGLE_LEFT_BLACK": "◀",
19
+ "TRIANGLE_LEFT_WHITE": "◁",
20
+ "TRIANGLE_RIGHT_BLACK": "▶",
21
+ "TRIANGLE_RIGHT_WHITE": "▷",
22
+ },
23
+ "PIPE_ASCII": {
24
+ "HORIZONTAL": "-",
25
+ "VERTICAL": "|",
26
+ "TOP_LEFT": "/",
27
+ "TOP_RIGHT": "\\",
28
+ "CROSS": "+",
29
+ },
30
+ "PIPE_EXTENDED": {
31
+ "HORIZONTAL": "─",
32
+ "VERTICAL": "│",
33
+ "BOTTOM_LEFT_TURN": "┌",
34
+ "BOTTOM_RIGHT_TURN": "┐",
35
+ "TOP_LEFT_TURN": "└",
36
+ "TOP_RIGHT_TURN": "┘",
37
+ "T_UP": "┴",
38
+ "T_DOWN": "┬",
39
+ "T_LEFT": "┤",
40
+ "T_RIGHT": "├",
41
+ "CROSS": "┼",
42
+ "HEAVY_HORIZONTAL": "━",
43
+ "HEAVY_VERTICAL": "┃",
44
+ "HEAVY_BOTTOM_LEFT_TURN": "┏",
45
+ "HEAVY_BOTTOM_RIGHT_TURN": "┓",
46
+ "HEAVY_TOP_LEFT_TURN": "┗",
47
+ "HEAVY_TOP_RIGHT_TURN": "┛",
48
+ "HEAVY_T_UP": "┻",
49
+ "HEAVY_T_DOWN": "┳",
50
+ "HEAVY_T_LEFT": "┫",
51
+ "HEAVY_T_RIGHT": "┣",
52
+ "HEAVY_CROSS": "╋",
53
+ "DOUBLE_HORIZONTAL": "═",
54
+ "DOUBLE_VERTICAL": "║",
55
+ "DOUBLE_BOTTOM_LEFT_TURN": "╔",
56
+ "DOUBLE_BOTTOM_RIGHT_TURN": "╗",
57
+ "DOUBLE_TOP_LEFT_TURN": "╚",
58
+ "DOUBLE_TOP_RIGHT_TURN": "╝",
59
+ "DOUBLE_T_UP": "╩",
60
+ "DOUBLE_T_DOWN": "╦",
61
+ "DOUBLE_T_LEFT": "╣",
62
+ "DOUBLE_T_RIGHT": "╠",
63
+ "DOUBLE_CROSS": "╬",
64
+ "ROUNDED_BOTTOM_LEFT_TURN": "╭",
65
+ "ROUNDED_BOTTOM_RIGHT_TURN": "╮",
66
+ "ROUNDED_TOP_LEFT_TURN": "╰",
67
+ "ROUNDED_TOP_RIGHT_TURN": "╯",
68
+ "ROUNDED_T_UP": "┴",
69
+ "ROUNDED_T_DOWN": "┬",
70
+ "ROUNDED_T_LEFT": "┤",
71
+ "ROUNDED_T_RIGHT": "├",
72
+ "ROUNDED_CROSS": "┼",
73
+ },
74
+ "PIPE_BLOCKS": {
75
+ "FULL_BLOCK": "█",
76
+ "LOWER_HALF_BLOCK": "▄",
77
+ "UPPER_HALF_BLOCK": "▀",
78
+ "LEFT_HALF_BLOCK": "▌",
79
+ "RIGHT_HALF_BLOCK": "▐",
80
+ "LOWER_LEFT_QUARTER_BLOCK": "▖",
81
+ "LOWER_RIGHT_QUARTER_BLOCK": "▗",
82
+ "UPPER_LEFT_QUARTER_BLOCK": "▘",
83
+ "UPPER_RIGHT_QUARTER_BLOCK": "▝",
84
+ "LEFT_THREE_QUARTER_BLOCK": "▙",
85
+ "RIGHT_THREE_QUARTER_BLOCK": "▛",
86
+ "LOWER_THREE_QUARTER_BLOCK": "▟",
87
+ "UPPER_THREE_QUARTER_BLOCK": "▜",
88
+ },
89
+ "PIPE_EXTRAS": {
90
+ "X": "╳",
91
+ "HALF_LEFT_HORIZONTAL": "╴",
92
+ "HALF_TOP_VERTICAL": "╵",
93
+ "HALF_RIGHT_HORIZONTAL": "╶",
94
+ "HALF_BOTTOM_VERTICAL": "╷",
95
+ "END_LEFT": "╼",
96
+ "END_TOP": "╽",
97
+ "END_RIGHT": "╾",
98
+ "END_BOTTOM": "╿",
99
+ },
100
+ "DOTS": {
101
+ "ELLIPSE_MIDDLE": "⋯",
102
+ "ELLIPSE_VERTICAL": "⋮",
103
+ "ELLIPSE_DIAGONAL_RIGHT_TO_LEFT": "⋰",
104
+ "ELLIPSE_DIAGONAL_LEFT_TO_RIGHT": "⋱",
105
+ "MIDDOT": "·",
106
+ "BULLET": "•",
107
+ },
108
+ "ARROWS": {
109
+ "UP_ARROW": "↑",
110
+ "DOWN_ARROW": "↓",
111
+ "LEFT_ARROW": "←",
112
+ "RIGHT_ARROW": "→",
113
+ "UP_DOWN_ARROW": "↕",
114
+ "LEFT_RIGHT_ARROW": "↔",
115
+ "NORTH_WEST_ARROW": "↖",
116
+ "NORTH_EAST_ARROW": "↗",
117
+ "SOUTH_EAST_ARROW": "↘",
118
+ "SOUTH_WEST_ARROW": "↙",
119
+ "DOUBLE_UP_ARROW": "⇑",
120
+ "DOUBLE_DOWN_ARROW": "⇓",
121
+ "DOUBLE_LEFT_ARROW": "⇐",
122
+ "DOUBLE_RIGHT_ARROW": "⇒",
123
+ "DOUBLE_LEFT_RIGHT_ARROW": "⇔",
124
+ "DOUBLE_UP_DOWN_ARROW": "⇕",
125
+ },
126
+ "BRAILLE": {i: chr(0x2800 + i) for i in range(1, 256)},
127
+ }
128
+
129
+ if __name__ == "__main__":
130
+ for category, chars in CharLib.items():
131
+ print(f"Category: {category}")
132
+ for name, char in chars.items():
133
+ print(f" {name}: {char}")
134
+ print()
libascii/color.py ADDED
@@ -0,0 +1,32 @@
1
+ class Color:
2
+ def __init__(self, value: str | tuple[int, int, int]) -> None:
3
+ if isinstance(value, tuple):
4
+ if len(value) != 3 or not all(
5
+ isinstance(c, int) and 0 <= c <= 255 for c in value
6
+ ):
7
+ msg = "RGB tuple must contain 3 integers in range 0-255"
8
+ raise ValueError(msg)
9
+ r, g, b = value
10
+ self.value = f"#{r:02x}{g:02x}{b:02x}"
11
+ elif value == "":
12
+ self.value = None
13
+ else:
14
+ self.value = value
15
+
16
+ @classmethod
17
+ def from_hex(cls, hex_str: str) -> "Color":
18
+ if not isinstance(hex_str, str) or not hex_str.startswith("#"):
19
+ msg = "hex string must be in format #RRGGBB"
20
+ raise ValueError(msg)
21
+ if len(hex_str) != 7:
22
+ msg = "hex string must be in format #RRGGBB"
23
+ raise ValueError(msg)
24
+ try:
25
+ int(hex_str[1:], 16)
26
+ except ValueError:
27
+ msg = f"invalid hex string: {hex_str!r}"
28
+ raise ValueError(msg) from None
29
+ return cls(hex_str)
30
+
31
+ def __str__(self) -> str:
32
+ return self.value if self.value is not None else ""
libascii/game.py ADDED
@@ -0,0 +1,204 @@
1
+ from __future__ import annotations
2
+
3
+ import tkinter as tk
4
+ from collections.abc import Callable
5
+
6
+ from .color import Color
7
+ from .grid import Grid
8
+ from .shapes.shape import Shape
9
+ from ._utils.keymapping import keybinding_to_events
10
+
11
+
12
+ class Game:
13
+ @staticmethod
14
+ def _prevent_selection(event: tk.Event[tk.Misc]) -> str:
15
+ return "break"
16
+
17
+ def __init__(
18
+ self,
19
+ width: int = 80,
20
+ height: int = 24,
21
+ title: str = "asciilib game",
22
+ fg: Color | None = None,
23
+ bg: Color | None = None,
24
+ filler: str = " ",
25
+ fps: int = 60,
26
+ ) -> None:
27
+ self.width = width
28
+ self.height = height
29
+
30
+ self.default_bg = bg if bg is not None else Color("black")
31
+ self.default_fg = fg if fg is not None else Color("white")
32
+
33
+ self.filler = filler
34
+ self.title = title
35
+
36
+ self.grid = Grid(width, height, filler)
37
+
38
+ self._key_down_bindings: list[tuple[str, Callable]] = []
39
+ self._key_up_bindings: list[tuple[str, Callable]] = []
40
+
41
+ self._mouse_down_bindings: list[Callable[[tuple[int, int]], None]] = []
42
+ self._mouse_up_bindings: list[Callable[[tuple[int, int]], None]] = []
43
+
44
+ self._frame_bindings: list[Callable[[], None]] = []
45
+ self._fps = fps
46
+
47
+ self._running = False
48
+ self.root: tk.Tk | None = None
49
+ self.area: tk.Text | None = None
50
+
51
+ def on_key_down(
52
+ self,
53
+ keybinding: str,
54
+ callback: Callable[[tk.Event[tk.Misc]], None],
55
+ ) -> None:
56
+ self._key_down_bindings.append((keybinding, callback))
57
+
58
+ def on_key_up(
59
+ self,
60
+ keybinding: str,
61
+ callback: Callable[[tk.Event[tk.Misc]], None],
62
+ ) -> None:
63
+ self._key_up_bindings.append((keybinding, callback))
64
+
65
+ def on_mouse_down(
66
+ self,
67
+ callback: Callable[[tuple[int, int]], None],
68
+ ) -> None:
69
+ self._mouse_down_bindings.append(callback)
70
+
71
+ def on_mouse_up(
72
+ self,
73
+ callback: Callable[[tuple[int, int]], None],
74
+ ) -> None:
75
+ self._mouse_up_bindings.append(callback)
76
+
77
+ def on_frame(self, callback: Callable[[], None]) -> None:
78
+ self._frame_bindings.append(callback)
79
+
80
+ def draw(self, shape: Shape) -> None:
81
+ shape.draw(self.grid)
82
+
83
+ def set(
84
+ self,
85
+ x: int,
86
+ y: int,
87
+ char: str = "",
88
+ color: Color | None = None,
89
+ ) -> None:
90
+ self.grid.set(x, y, char, color)
91
+
92
+ def get(self, x: int, y: int) -> tuple[str, Color | None]:
93
+ return self.grid.get(x, y)
94
+
95
+ def clear(self) -> None:
96
+ self.grid.clear()
97
+
98
+ def text(
99
+ self,
100
+ x: int,
101
+ y: int,
102
+ string: str,
103
+ color: Color | None = None,
104
+ ) -> None:
105
+ for offset, char in enumerate(string):
106
+ self.grid.set(x + offset, y, char, color)
107
+
108
+ def refresh(self) -> None:
109
+ self._render()
110
+
111
+ @property
112
+ def fps(self) -> int:
113
+ return self._fps
114
+
115
+ @fps.setter
116
+ def fps(self, value: int) -> None:
117
+ self._fps = value
118
+
119
+ def stop(self) -> None:
120
+ if not self._running:
121
+ return
122
+ self._running = False
123
+ if self.root is not None:
124
+ self.root.quit()
125
+
126
+ def start(self) -> None:
127
+ root = tk.Tk()
128
+ root.title(self.title)
129
+ root.protocol("WM_DELETE_WINDOW", self.stop)
130
+ self.root = root
131
+
132
+ self.area = tk.Text(
133
+ self.root,
134
+ width=self.width,
135
+ height=self.height,
136
+ font=("Courier", 10),
137
+ bg=str(self.default_bg),
138
+ fg=str(self.default_fg),
139
+ padx=0,
140
+ pady=0,
141
+ borderwidth=0,
142
+ highlightthickness=0,
143
+ )
144
+ self.area.pack()
145
+
146
+ self.area.bind("<Button-1>", self._prevent_selection)
147
+ self.area.bind("<B1-Motion>", self._prevent_selection)
148
+
149
+ for keybinding, callback in self._key_down_bindings:
150
+ for event in keybinding_to_events(keybinding, "KeyPress"):
151
+ self.area.bind(event, callback)
152
+
153
+ for keybinding, callback in self._key_up_bindings:
154
+ for event in keybinding_to_events(keybinding, "KeyRelease"):
155
+ self.area.bind(event, callback)
156
+
157
+ def _make_mouse_handler(
158
+ bindings: list[Callable[[tuple[int, int]], None]],
159
+ ) -> Callable[[tk.Event[tk.Misc]], None]:
160
+ def handler(event: tk.Event[tk.Misc]) -> None:
161
+ line, col = self.area.index(f"@{event.x},{event.y}").split(".")
162
+ coord = (int(col), int(line) - 1)
163
+ for binding in bindings:
164
+ binding(coord)
165
+
166
+ return handler
167
+
168
+ down_handler = _make_mouse_handler(self._mouse_down_bindings)
169
+ up_handler = _make_mouse_handler(self._mouse_up_bindings)
170
+ self.area.bind("<Button-1>", down_handler)
171
+ self.area.bind("<ButtonRelease-1>", up_handler)
172
+
173
+ self._running = True
174
+ self._render()
175
+
176
+ def _tick() -> None:
177
+ if not self._running:
178
+ return
179
+ for callback in self._frame_bindings:
180
+ callback()
181
+ self.root.after(round(1000 / self._fps), _tick)
182
+
183
+ self.root.after(round(1000 / self._fps), _tick)
184
+ self.root.mainloop()
185
+
186
+ def _render(self) -> None:
187
+ self.area.delete("1.0", tk.END)
188
+
189
+ for y, row in enumerate(self.grid.content):
190
+ self.area.insert(f"{y + 1}.0", "".join(row) + "\n")
191
+ for y, row in enumerate(self.grid.colors):
192
+ for x, color in enumerate(row):
193
+ if color is not None:
194
+ color_str = str(color)
195
+
196
+ if color_str:
197
+ self.area.tag_add(
198
+ color_str, f"{y + 1}.{x}", f"{y + 1}.{x + 1}"
199
+ )
200
+ self.area.tag_config(color_str, foreground=color_str)
201
+
202
+
203
+ if __name__ == "__main__":
204
+ Game().start()
libascii/grid.py ADDED
@@ -0,0 +1,45 @@
1
+ from libascii.color import Color
2
+
3
+
4
+ class Grid:
5
+ def __init__(self, width: int, height: int, filler: str = " ") -> None:
6
+ self.width = width
7
+ self.height = height
8
+ self.filler = filler
9
+ self.content: list[list[str]] = [
10
+ [filler for _ in range(width)] for _ in range(height)
11
+ ]
12
+ self.colors: list[list[Color | None]] = [
13
+ [None for _ in range(width)] for _ in range(height)
14
+ ]
15
+
16
+ def in_bounds(self, x: int, y: int) -> bool:
17
+ return 0 <= x < self.width and 0 <= y < self.height
18
+
19
+ def set(
20
+ self,
21
+ x: int,
22
+ y: int,
23
+ char: str = "",
24
+ color: Color | None = None,
25
+ ) -> None:
26
+ self.content[y][x] = char
27
+ self.colors[y][x] = color
28
+
29
+ def set_many(
30
+ self, cells: list[tuple[int, int, str, Color | None]]
31
+ ) -> None:
32
+ for x, y, char, color in cells:
33
+ self.set(x, y, char, color)
34
+
35
+ def get(self, x: int, y: int) -> tuple[str, Color | None]:
36
+ return self.content[y][x], self.colors[y][x]
37
+
38
+ def fill(self, char: str = "", color: Color | None = None) -> None:
39
+ for y in range(self.height):
40
+ for x in range(self.width):
41
+ self.content[y][x] = char
42
+ self.colors[y][x] = color
43
+
44
+ def clear(self) -> None:
45
+ self.fill(self.filler, None)
libascii/py.typed ADDED
File without changes
@@ -0,0 +1,5 @@
1
+ from .shape import Shape
2
+ from .rectangle import Rectangle
3
+ from .ellipse import Ellipse
4
+ from .triangle import Triangle
5
+ from .line import Line
@@ -0,0 +1,70 @@
1
+ from ..color import Color
2
+ from ..grid import Grid
3
+ from .shape import Shape
4
+
5
+
6
+ class Ellipse(Shape):
7
+ def __init__(
8
+ self,
9
+ width: int,
10
+ height: int,
11
+ fill: str = "",
12
+ border: str = "",
13
+ fill_color: Color = Color(""),
14
+ border_color: Color = Color(""),
15
+ border_top: str = "",
16
+ border_left: str = "",
17
+ border_right: str = "",
18
+ border_bottom: str = "",
19
+ ) -> None:
20
+ self.width = width
21
+ self.height = height
22
+ self.fill = fill
23
+ self.border = border
24
+ self.fill_color = fill_color
25
+ self.border_color = border_color
26
+ self.border_top = border_top
27
+ self.border_left = border_left
28
+ self.border_right = border_right
29
+ self.border_bottom = border_bottom
30
+
31
+ def draw(self, grid: Grid) -> None:
32
+ if self.width == 0 or self.height == 0:
33
+ return
34
+ cx = (self.width - 1) / 2
35
+ cy = (self.height - 1) / 2
36
+ rx = max((self.width - 1) / 2, 1e-9)
37
+ ry = max((self.height - 1) / 2, 1e-9)
38
+
39
+ def inside(x: int, y: int) -> bool:
40
+ if x < 0 or x >= self.width or y < 0 or y >= self.height:
41
+ return False
42
+ dx = (x - cx) / rx
43
+ dy = (y - cy) / ry
44
+ return dx * dx + dy * dy <= 1
45
+
46
+ for y in range(self.height):
47
+ for x in range(self.width):
48
+ if not inside(x, y):
49
+ continue
50
+ on_border = not (
51
+ inside(x - 1, y)
52
+ and inside(x + 1, y)
53
+ and inside(x, y - 1)
54
+ and inside(x, y + 1)
55
+ )
56
+ if on_border:
57
+ grid.set(x, y, self._border_char(x, y), self.border_color)
58
+ else:
59
+ grid.set(x, y, self.fill, self.fill_color)
60
+
61
+ def _border_char(self, x: int, y: int) -> str:
62
+ if y == 0 and self.border_top:
63
+ return self.border_top
64
+ if y == self.height - 1 and self.border_bottom:
65
+ return self.border_bottom
66
+ if x == 0 and self.border_left:
67
+ return self.border_left
68
+ if x == self.width - 1 and self.border_right:
69
+ return self.border_right
70
+ return self.border
@@ -0,0 +1,42 @@
1
+ from ..color import Color
2
+ from ..grid import Grid
3
+ from .shape import Shape
4
+
5
+
6
+ class Line(Shape):
7
+ def __init__(
8
+ self,
9
+ x1: int,
10
+ y1: int,
11
+ x2: int,
12
+ y2: int,
13
+ char: str = "#",
14
+ color: Color = Color(""),
15
+ ) -> None:
16
+ self.x1 = x1
17
+ self.y1 = y1
18
+ self.x2 = x2
19
+ self.y2 = y2
20
+ self.char = char
21
+ self.color = color
22
+
23
+ def draw(self, grid: Grid) -> None:
24
+ x0, y0 = self.x1, self.y1
25
+ x1, y1 = self.x2, self.y2
26
+ dx = abs(x1 - x0)
27
+ dy = abs(y1 - y0)
28
+ sx = 1 if x0 < x1 else -1
29
+ sy = 1 if y0 < y1 else -1
30
+ err = dx - dy
31
+
32
+ while True:
33
+ grid.set(x0, y0, self.char, self.color)
34
+ if x0 == x1 and y0 == y1:
35
+ break
36
+ e2 = 2 * err
37
+ if e2 > -dy:
38
+ err -= dy
39
+ x0 += sx
40
+ if e2 < dx:
41
+ err += dx
42
+ y0 += sy
@@ -0,0 +1,54 @@
1
+ from ..color import Color
2
+ from ..grid import Grid
3
+ from .shape import Shape
4
+
5
+
6
+ class Rectangle(Shape):
7
+ def __init__(
8
+ self,
9
+ width: int,
10
+ height: int,
11
+ fill: str = "",
12
+ border: str = "",
13
+ fill_color: Color = Color(""),
14
+ border_color: Color = Color(""),
15
+ border_top: str = "",
16
+ border_left: str = "",
17
+ border_right: str = "",
18
+ border_bottom: str = "",
19
+ ) -> None:
20
+ self.width = width
21
+ self.height = height
22
+ self.fill = fill
23
+ self.border = border
24
+ self.fill_color = fill_color
25
+ self.border_color = border_color
26
+ self.border_top = border_top
27
+ self.border_left = border_left
28
+ self.border_right = border_right
29
+ self.border_bottom = border_bottom
30
+
31
+ def draw(self, grid: Grid) -> None:
32
+ for y in range(self.height):
33
+ for x in range(self.width):
34
+ on_border = (
35
+ x == 0
36
+ or x == self.width - 1
37
+ or y == 0
38
+ or y == self.height - 1
39
+ )
40
+ if on_border:
41
+ grid.set(x, y, self._border_char(x, y), self.border_color)
42
+ else:
43
+ grid.set(x, y, self.fill, self.fill_color)
44
+
45
+ def _border_char(self, x: int, y: int) -> str:
46
+ if y == 0 and self.border_top:
47
+ return self.border_top
48
+ if y == self.height - 1 and self.border_bottom:
49
+ return self.border_bottom
50
+ if x == 0 and self.border_left:
51
+ return self.border_left
52
+ if x == self.width - 1 and self.border_right:
53
+ return self.border_right
54
+ return self.border
@@ -0,0 +1,8 @@
1
+ from abc import ABC, abstractmethod
2
+ from libascii.grid import Grid
3
+
4
+
5
+ class Shape(ABC):
6
+ @abstractmethod
7
+ def draw(self, grid: Grid) -> None:
8
+ pass
@@ -0,0 +1,84 @@
1
+ from ..color import Color
2
+ from ..grid import Grid
3
+ from .shape import Shape
4
+
5
+
6
+ class Triangle(Shape):
7
+ def __init__(
8
+ self,
9
+ width: int,
10
+ height: int,
11
+ fill: str = "",
12
+ border: str = "",
13
+ fill_color: Color = Color(""),
14
+ border_color: Color = Color(""),
15
+ border_left: str = "",
16
+ border_right: str = "",
17
+ border_bottom: str = "",
18
+ ) -> None:
19
+ self.width = width
20
+ self.height = height
21
+ self.fill = fill
22
+ self.border = border
23
+ self.fill_color = fill_color
24
+ self.border_color = border_color
25
+ self.border_left = border_left
26
+ self.border_right = border_right
27
+ self.border_bottom = border_bottom
28
+
29
+ def draw(self, grid: Grid) -> None:
30
+ if self.height == 0 or self.width == 0:
31
+ return
32
+
33
+ top = (self.width // 2, 0)
34
+ bottom_left = (0, self.height - 1)
35
+ bottom_right = (self.width - 1, self.height - 1)
36
+
37
+ def edge_x(
38
+ a: tuple[int, int],
39
+ b: tuple[int, int],
40
+ y: int,
41
+ ) -> float | None:
42
+ (x0, y0), (x1, y1) = a, b
43
+ if y0 == y1 or y < min(y0, y1) or y > max(y0, y1):
44
+ return None
45
+ return x0 + (x1 - x0) * (y - y0) / (y1 - y0)
46
+
47
+ edges = (
48
+ (top, bottom_left),
49
+ (top, bottom_right),
50
+ (bottom_left, bottom_right),
51
+ )
52
+
53
+ for y in range(self.height):
54
+ xs = []
55
+ for a, b in edges:
56
+ x = edge_x(a, b, y)
57
+ if x is not None:
58
+ xs.append(x)
59
+ if len(xs) < 2:
60
+ continue
61
+ left = int(round(min(xs)))
62
+ right = int(round(max(xs)))
63
+ for x in range(left, right + 1):
64
+ if x < 0 or x >= self.width:
65
+ continue
66
+ on_border = x == left or x == right or y == self.height - 1
67
+ if on_border:
68
+ grid.set(
69
+ x,
70
+ y,
71
+ self._border_char(x, y, left, right),
72
+ self.border_color,
73
+ )
74
+ else:
75
+ grid.set(x, y, self.fill, self.fill_color)
76
+
77
+ def _border_char(self, x: int, y: int, left: int, right: int) -> str:
78
+ if y == self.height - 1 and self.border_bottom:
79
+ return self.border_bottom
80
+ if x == left and self.border_left:
81
+ return self.border_left
82
+ if x == right and self.border_right:
83
+ return self.border_right
84
+ return self.border
libascii/sound.py ADDED
@@ -0,0 +1,24 @@
1
+ from pathlib import Path
2
+
3
+ import simpleaudio as sa
4
+
5
+
6
+ class Sound:
7
+ def __init__(self, path: str | Path) -> None:
8
+ self.wave_object = sa.WaveObject.from_wave_file(str(path))
9
+
10
+ @classmethod
11
+ def from_wav(cls, path: str | Path) -> "Sound":
12
+ return cls(path)
13
+
14
+ def play(self) -> sa.PlayObject:
15
+ return self.wave_object.play()
16
+
17
+ def stop(self, play_object: sa.PlayObject) -> None:
18
+ play_object.stop()
19
+
20
+ def is_playing(self, play_object: sa.PlayObject) -> bool:
21
+ return play_object.is_playing()
22
+
23
+ def wait_done(self, play_object: sa.PlayObject) -> None:
24
+ play_object.wait_done()
@@ -0,0 +1,27 @@
1
+ Metadata-Version: 2.4
2
+ Name: libascii
3
+ Version: 0.1.0
4
+ Summary:
5
+ Author: NNlk05
6
+ License-Expression: MIT OR Zlib
7
+ License-File: LICENSE
8
+ Classifier: Development Status :: 4 - Beta
9
+ Classifier: Intended Audience :: Developers
10
+ Classifier: Natural Language :: English
11
+ Classifier: Operating System :: OS Independent
12
+ Classifier: Programming Language :: Python :: 3 :: Only
13
+ Classifier: Programming Language :: Python :: 3.13
14
+ Classifier: Programming Language :: Python :: 3.14
15
+ Classifier: Programming Language :: Python :: 3.15
16
+ Classifier: Programming Language :: Python :: 3.16
17
+ Classifier: Programming Language :: Python :: Implementation :: CPython
18
+ Classifier: Topic :: Games/Entertainment
19
+ Classifier: Topic :: Games/Entertainment :: Arcade
20
+ Classifier: Topic :: Games/Entertainment :: Board Games
21
+ Classifier: Topic :: Games/Entertainment :: Multi-User Dungeons (MUD)
22
+ Classifier: Topic :: Games/Entertainment :: Puzzle Games
23
+ Classifier: Typing :: Typed
24
+ Requires-Dist: simpleaudio>=1.0.4
25
+ Requires-Python: >=3.10
26
+ Description-Content-Type: text/markdown
27
+
@@ -0,0 +1,19 @@
1
+ libascii/__init__.py,sha256=MTormQASUYaytNh8YXveW0lGwc4rg3-cyMNKbvYACA8,2276
2
+ libascii/_utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
3
+ libascii/_utils/keymapping.py,sha256=HtUOTtiEIsqA5X-Tn2jKFFALEeYt3ls8oCcheNSo4-U,2196
4
+ libascii/chars.py,sha256=vWS0higBty76cFiqPJjVw7mrJX_2ONiEKz6jL7QiLDY,4225
5
+ libascii/color.py,sha256=Q0vFwz5xCFbJ4dv4758Qprppir1mCLIcOP-J4Gujnfk,1173
6
+ libascii/game.py,sha256=EKIbBsccpELysKe7IukxMmoW3oam42i4M9VCv9x6nTk,5917
7
+ libascii/grid.py,sha256=G6-RyGUNm4AYpG118BncwMsbiQeTzaT-mGmOJgyXqEg,1357
8
+ libascii/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
9
+ libascii/shapes/__init__.py,sha256=Q0F6Zv28ECoStbxqqTo_WsxX19q-v1yAiXAN5h-p7A8,141
10
+ libascii/shapes/ellipse.py,sha256=EDozWxDcbutxsZhNc9tmeC15We__JU4oECEsxWWhYGM,2271
11
+ libascii/shapes/line.py,sha256=Ch5oQVlTB02PwefWovoDLaJwkBCIisO3hZFRyDr-K4k,966
12
+ libascii/shapes/rectangle.py,sha256=n_Tia3WYw--qvH4Kix9r-LbZHeC-Rj9lvVpWF327iZg,1711
13
+ libascii/shapes/shape.py,sha256=5gjRUWYCChIc6c6oXdIRnPWp90O6WiigacfMWtr3BOg,160
14
+ libascii/shapes/triangle.py,sha256=1bMBLZmCMwDjFCIAJxyreJTZ64arF2SnUdtesDGbC7I,2624
15
+ libascii/sound.py,sha256=AEGKNLgTt8pHR2c-Ea-HgiB70RqV8kOfPlCqqHL7wYY,636
16
+ libascii-0.1.0.dist-info/licenses/LICENSE,sha256=gvoF8ebg3xSObRCtcOGNXBpA91LQ-gysMpCisjyGlNs,1960
17
+ libascii-0.1.0.dist-info/WHEEL,sha256=MWx7psoF7KNST9G72pGtiBYuaXRXQXBk9pw5R6FzYko,80
18
+ libascii-0.1.0.dist-info/METADATA,sha256=ia1CfLFd6FO8sWUON4_AgFvupAWChGU3ri6lxFAD8X4,1039
19
+ libascii-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: uv 0.13.7
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,21 @@
1
+ This project is dual-licensed under MIT or zlib.
2
+
3
+ zlib license:
4
+ Copyright (c) 2026 NNlk05
5
+
6
+ This software is provided 'as-is', without any express or implied warranty. In no event will the authors be held liable for any damages arising from the use of this software.
7
+
8
+ Permission is granted to anyone to use this software for any purpose, including commercial applications, and to alter it and redistribute it freely, subject to the following restrictions:
9
+
10
+ 1. The origin of this software must not be misrepresented; you must not claim that you wrote the original software. If you use this software in a product, an acknowledgment in the product documentation would be appreciated but is not required.
11
+ 2. Altered source versions must be plainly marked as such, and must not be misrepresented as being the original software.
12
+ 3. This notice may not be removed or altered from any source distribution.
13
+
14
+ MIT license:
15
+ Copyright (c) 2026 NNlk05
16
+
17
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
18
+
19
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
20
+
21
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.