rolly 1.0.0__tar.gz

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.
rolly-1.0.0/PKG-INFO ADDED
@@ -0,0 +1,94 @@
1
+ Metadata-Version: 2.3
2
+ Name: rolly
3
+ Version: 1.0.0
4
+ Summary: Roll dice on the command line because why not?
5
+ Author: George Lesica
6
+ Author-email: George Lesica <george@lesica.com>
7
+ Requires-Dist: click>=8.3.0
8
+ Requires-Python: >=3.14
9
+ Description-Content-Type: text/markdown
10
+
11
+ ![Rolly Mascot](mascot.png)
12
+
13
+ # Rolly
14
+
15
+ Rolling dice the boring way.
16
+
17
+ ## Introduction
18
+
19
+ A roll is expressed as a single string of text that contains a
20
+ sequence of instructions. Whitespace between instructions is entirely
21
+ ignored. There are different ways to express the same roll,
22
+ some being shorter than others. Below, we roll 3 D20s with
23
+ advantage in three different ways.
24
+
25
+ ```
26
+ d20 d20 d20+
27
+
28
+ d20 x3+
29
+
30
+ d20 d20x2 +
31
+ ```
32
+
33
+ ## Instructions
34
+
35
+ The available instructions are listed below.
36
+
37
+ * A die to be rolled: `dN`, `N` is the number of sides
38
+ * Roll multiple dice: `xM`, `M` is the number of the previous die to roll
39
+ * Advantage on the roll: `+` anywhere in the string
40
+ * Disadvantage on the roll: `-` anywhere in the string
41
+
42
+ ## Examples
43
+
44
+ So a number prefixed with `d` implies `x1` and a space between a number
45
+ and a count is ignored. Advantage is indicated by a `+` anywhere in the
46
+ string, disadvantage is a `-`.
47
+
48
+ ```
49
+ d20x2 +
50
+ a d20 d20
51
+ d20x2-
52
+ ```
53
+
54
+ ## CLI
55
+
56
+ The CLI is straightforward. Once the package is installed, it can be run
57
+ as a module:
58
+
59
+ ```
60
+ $ python -m rolly
61
+ ```
62
+
63
+ This will show the help text. The primary command is `roll`, which takes
64
+ a string of instructions and displays the result.
65
+
66
+ ```
67
+ $ python -m rolly roll d20x3
68
+ ▄▄▄▄ ▄▄▄ ▄▄▄▄▄▄▄▄ ▄▄▄▄▄
69
+ ██▀▀▀█ █▀██ ▀▀▀▀▀███ █▀▀▀▀██▄
70
+ ██ ▄▄▄ ██ ▄██ ▄██
71
+ ███▀▀██▄ ██ ██ █████
72
+ ██ ██ ██ ██ ▀██
73
+ ▀██▄▄██▀ ▄▄▄██▄▄▄ ██ █▄▄▄▄██▀
74
+ ▀▀▀▀ ▀▀▀▀▀▀▀▀ ▀▀ ▀▀▀▀▀
75
+ ```
76
+
77
+ The default uses ASCII art, but passing the `--plain` (`-p`) flag will
78
+ cause it to use plain text.
79
+ ```
80
+ $ python -m rolly roll d20x3
81
+ 6 17 3
82
+ ```
83
+
84
+ It is also possible to sum the dice automatically using the `--add` (`-a`)
85
+ flag:
86
+
87
+ ```
88
+ $ python -m rolly roll -ap d20x3
89
+ 16 12 7
90
+ = 35
91
+ ```
92
+
93
+ The ASCII art theme can be changed with the `--theme` (`-t`) option and
94
+ the available themes can be listed with the `themes` command.
rolly-1.0.0/README.md ADDED
@@ -0,0 +1,84 @@
1
+ ![Rolly Mascot](mascot.png)
2
+
3
+ # Rolly
4
+
5
+ Rolling dice the boring way.
6
+
7
+ ## Introduction
8
+
9
+ A roll is expressed as a single string of text that contains a
10
+ sequence of instructions. Whitespace between instructions is entirely
11
+ ignored. There are different ways to express the same roll,
12
+ some being shorter than others. Below, we roll 3 D20s with
13
+ advantage in three different ways.
14
+
15
+ ```
16
+ d20 d20 d20+
17
+
18
+ d20 x3+
19
+
20
+ d20 d20x2 +
21
+ ```
22
+
23
+ ## Instructions
24
+
25
+ The available instructions are listed below.
26
+
27
+ * A die to be rolled: `dN`, `N` is the number of sides
28
+ * Roll multiple dice: `xM`, `M` is the number of the previous die to roll
29
+ * Advantage on the roll: `+` anywhere in the string
30
+ * Disadvantage on the roll: `-` anywhere in the string
31
+
32
+ ## Examples
33
+
34
+ So a number prefixed with `d` implies `x1` and a space between a number
35
+ and a count is ignored. Advantage is indicated by a `+` anywhere in the
36
+ string, disadvantage is a `-`.
37
+
38
+ ```
39
+ d20x2 +
40
+ a d20 d20
41
+ d20x2-
42
+ ```
43
+
44
+ ## CLI
45
+
46
+ The CLI is straightforward. Once the package is installed, it can be run
47
+ as a module:
48
+
49
+ ```
50
+ $ python -m rolly
51
+ ```
52
+
53
+ This will show the help text. The primary command is `roll`, which takes
54
+ a string of instructions and displays the result.
55
+
56
+ ```
57
+ $ python -m rolly roll d20x3
58
+ ▄▄▄▄ ▄▄▄ ▄▄▄▄▄▄▄▄ ▄▄▄▄▄
59
+ ██▀▀▀█ █▀██ ▀▀▀▀▀███ █▀▀▀▀██▄
60
+ ██ ▄▄▄ ██ ▄██ ▄██
61
+ ███▀▀██▄ ██ ██ █████
62
+ ██ ██ ██ ██ ▀██
63
+ ▀██▄▄██▀ ▄▄▄██▄▄▄ ██ █▄▄▄▄██▀
64
+ ▀▀▀▀ ▀▀▀▀▀▀▀▀ ▀▀ ▀▀▀▀▀
65
+ ```
66
+
67
+ The default uses ASCII art, but passing the `--plain` (`-p`) flag will
68
+ cause it to use plain text.
69
+ ```
70
+ $ python -m rolly roll d20x3
71
+ 6 17 3
72
+ ```
73
+
74
+ It is also possible to sum the dice automatically using the `--add` (`-a`)
75
+ flag:
76
+
77
+ ```
78
+ $ python -m rolly roll -ap d20x3
79
+ 16 12 7
80
+ = 35
81
+ ```
82
+
83
+ The ASCII art theme can be changed with the `--theme` (`-t`) option and
84
+ the available themes can be listed with the `themes` command.
@@ -0,0 +1,24 @@
1
+ [project]
2
+ name = "rolly"
3
+ version = "1.0.0"
4
+ description = "Roll dice on the command line because why not?"
5
+ readme = "README.md"
6
+ authors = [
7
+ { name = "George Lesica", email = "george@lesica.com" }
8
+ ]
9
+ requires-python = ">=3.14"
10
+ dependencies = [
11
+ "click>=8.3.0",
12
+ ]
13
+
14
+ [project.scripts]
15
+ rolly = "rolly:main"
16
+
17
+ [build-system]
18
+ requires = ["uv_build>=0.9.7,<0.10.0"]
19
+ build-backend = "uv_build"
20
+
21
+ [dependency-groups]
22
+ dev = [
23
+ "pytest>=8.4.2",
24
+ ]
@@ -0,0 +1,27 @@
1
+ from .roll import (
2
+ Roll as Roll,
3
+ )
4
+
5
+ from .output import (
6
+ Formatter as Formatter,
7
+ ArtFormatter as ArtFormatter,
8
+ TextFormatter as TextFormatter,
9
+ )
10
+
11
+ from .themes import (
12
+ Theme as Theme,
13
+ get_theme as get_theme,
14
+ )
15
+
16
+
17
+ # CLI interface for Rolly
18
+
19
+ def main():
20
+ import click
21
+
22
+ from .cli import entry
23
+
24
+ try:
25
+ entry()
26
+ except RuntimeError as e:
27
+ click.echo(e)
@@ -0,0 +1,55 @@
1
+ from dataclasses import dataclass
2
+ from typing import Dict
3
+
4
+
5
+ @dataclass
6
+ class Character:
7
+ _data: Dict
8
+
9
+ _abil: Dict[str, int]
10
+ _save: Dict[str, int]
11
+
12
+ def __init__(self, path: str):
13
+ with open(path, 'rb') as f:
14
+ import tomllib
15
+ data = tomllib.load(f)
16
+
17
+ for sec, data in data:
18
+ match (sec, data):
19
+ case ('abilities', {
20
+ 'str': int(),
21
+ 'dex': int(),
22
+ 'con': int(),
23
+ 'int': int(),
24
+ 'wis': int(),
25
+ 'cha': int(),
26
+ }):
27
+ for attr in ('str', 'dex', 'con', 'int', 'wis', 'cha'):
28
+ self._abil[attr] = data[attr]
29
+
30
+ case ('saves', {
31
+ 'str': int(),
32
+ 'dex': int(),
33
+ 'con': int(),
34
+ 'int': int(),
35
+ 'wis': int(),
36
+ 'cha': int(),
37
+ }):
38
+ for attr in ('str', 'dex', 'con', 'int', 'wis', 'cha'):
39
+ self._save[attr] = data[attr]
40
+
41
+ case _:
42
+ raise RuntimeError(f'invalid character section: {sec}')
43
+
44
+ def has_attr(self, attr: str) -> bool:
45
+ return attr in self._abil or attr in self._save
46
+
47
+ def abil_val(self, attr: str) -> int:
48
+ if self.has_attr(attr):
49
+ return self._abil[attr]
50
+ return 0
51
+
52
+ def save_val(self, attr: str) -> int:
53
+ if self.has_attr(attr):
54
+ return self._save[attr]
55
+ return 0
@@ -0,0 +1,73 @@
1
+ from typing import Tuple
2
+
3
+ import click
4
+
5
+
6
+ @click.group()
7
+ def entry():
8
+ pass
9
+
10
+
11
+ @entry.command()
12
+ def themes():
13
+ from .themes import get_themes
14
+
15
+ click.echo("Available themes:")
16
+ for name in get_themes():
17
+ click.echo(f" {name}")
18
+
19
+
20
+ # TODO: The formatter (and theme) can be determined automatically
21
+ # Use click to decode the value passed (I think)
22
+
23
+ @entry.command()
24
+ @click.option("--plain", "-p", is_flag=True, flag_value=True, help="Display the coin flip as plain text")
25
+ @click.option("--theme", "-t", type=str, default='default', help="Name of the display theme to use")
26
+ def flip(plain: bool, theme: str):
27
+ from .roll import Roll
28
+ from .output import TextFormatter, ArtFormatter
29
+ from .themes import get_theme
30
+
31
+ if plain:
32
+ fmt = TextFormatter()
33
+ else:
34
+ t = get_theme(theme)
35
+ fmt = ArtFormatter(t)
36
+
37
+ letters = {1: 'H', 2: 'T'}
38
+
39
+ v = Roll("d2").roll()[0]
40
+
41
+ out = fmt.text(letters[v])
42
+ click.echo(out)
43
+
44
+
45
+ @entry.command()
46
+ @click.argument("s", type=str, nargs=-1, required=True)
47
+ @click.option("--add", "-a", is_flag=True, flag_value=True, help="Add up the dice and display a total")
48
+ @click.option("--plain", "-p", is_flag=True, flag_value=True, help="Display the roll as plain text")
49
+ @click.option("--theme", "-t", type=str, default='default', help="Name of the display theme to use")
50
+ def roll(s: Tuple[str, ...], add: bool, plain: bool, theme: str):
51
+ from .roll import Roll
52
+ from .output import TextFormatter, ArtFormatter
53
+ from .themes import get_theme
54
+
55
+ if plain:
56
+ fmt = TextFormatter()
57
+ else:
58
+ t = get_theme(theme)
59
+ fmt = ArtFormatter(t)
60
+
61
+ in_str = ' '.join(s)
62
+
63
+ r = Roll()
64
+ r.parse(in_str)
65
+
66
+ vs = r.roll()
67
+
68
+ out = ' '.join(str(v) for v in vs)
69
+ if add:
70
+ out += f'\n= {str(sum(vs))}'
71
+
72
+ fmt_out = fmt.text(out)
73
+ click.echo(fmt_out)
@@ -0,0 +1,39 @@
1
+ from typing import List, Protocol
2
+
3
+ from .themes import Theme
4
+
5
+
6
+ class Formatter(Protocol):
7
+ def nums(self, vs: List[int], sep: str = '') -> str:
8
+ raise NotImplementedError
9
+
10
+ def num(self, v: int) -> str:
11
+ raise NotImplementedError
12
+
13
+ def text(self, s: str) -> str:
14
+ raise NotImplementedError
15
+
16
+
17
+ class ArtFormatter(Formatter):
18
+ def __init__(self, theme: Theme):
19
+ self._theme = theme
20
+
21
+ def nums(self, vs: List[int], sep: str = '') -> str:
22
+ return self._theme.render(sep.join(str(v) for v in vs))
23
+
24
+ def num(self, v: int) -> str:
25
+ return self.nums([v])
26
+
27
+ def text(self, s: str) -> str:
28
+ return self._theme.render(s)
29
+
30
+
31
+ class TextFormatter(Formatter):
32
+ def nums(self, vs: List[int], sep: str = '') -> str:
33
+ return sep.join(str(v) for v in vs)
34
+
35
+ def num(self, v: int) -> str:
36
+ return self.nums([v])
37
+
38
+ def text(self, s: str) -> str:
39
+ return s
File without changes
@@ -0,0 +1,147 @@
1
+ from dataclasses import dataclass
2
+ from secrets import randbelow
3
+ from typing import List, Callable
4
+
5
+ from .char import Character
6
+
7
+ # A Roller just accepts a maximum value and returns a random
8
+ # value in the closed interval [1, max], to simulate rolling
9
+ # a die with max sides.
10
+ Roller = Callable[[int], int]
11
+
12
+
13
+ def default_roller(bound: int) -> int:
14
+ return randbelow(bound) + 1
15
+
16
+
17
+ @dataclass
18
+ class Roll:
19
+ _dice: List[int]
20
+ _roller: Roller
21
+ _adv: bool = False
22
+ _dis: bool = False
23
+ _bonus: int = 0
24
+
25
+ def __init__(self, roll: str = '', roller: Roller = default_roller):
26
+ self._dice = []
27
+ self._roller = roller
28
+
29
+ if roll != '':
30
+ self.parse(roll)
31
+
32
+ @property
33
+ def bonus(self) -> int:
34
+ return self._bonus
35
+
36
+ def add_die(self, value: int):
37
+ self._dice.append(value)
38
+
39
+ def advantage(self, value: bool):
40
+ self._adv = value
41
+
42
+ def disadvantage(self, value: bool):
43
+ self._dis = value
44
+
45
+ def roll(self) -> List[int]:
46
+ one = self._roll()
47
+
48
+ if self._adv and not self._dis:
49
+ two = self._roll()
50
+ if sum(two) > sum(one):
51
+ return two
52
+
53
+ if self._dis and not self._adv:
54
+ two = self._roll()
55
+ if sum(two) < sum(one):
56
+ return two
57
+
58
+ return one
59
+
60
+ def _roll(self) -> List[int]:
61
+ values = []
62
+
63
+ for d in self._dice:
64
+ value = self._roller(d)
65
+ values.append(value)
66
+
67
+ return values
68
+
69
+ def reset(self):
70
+ self._dice.clear()
71
+ self._adv = False
72
+ self._dis = False
73
+ self._bonus = 0
74
+
75
+ def parse(self, roll: str, character: Character | None = None):
76
+ self.reset()
77
+
78
+ # Burn whitespace from the ends to handle line endings
79
+ roll = roll.strip()
80
+
81
+ attr = ''
82
+ save = False
83
+
84
+ while len(roll) > 0:
85
+ if roll.startswith('d'):
86
+ # Reading a die
87
+ roll = roll[1:]
88
+
89
+ value = 0
90
+ while len(roll) > 0 and roll[0] in '0123456789':
91
+ value = value * 10 + int(roll[0])
92
+ roll = roll[1:]
93
+
94
+ self._dice.append(value)
95
+ continue
96
+
97
+ if roll.startswith('x'):
98
+ # Reading a count for the previous die
99
+ roll = roll[1:]
100
+
101
+ if len(self._dice) == 0:
102
+ raise RuntimeError('invalid syntax: count found before dice')
103
+
104
+ value = 0
105
+ while len(roll) > 0 and roll[0] in '0123456789':
106
+ value = value * 10 + int(roll[0])
107
+ roll = roll[1:]
108
+
109
+ self._dice.extend([self._dice[-1]] * (value - 1))
110
+ continue
111
+
112
+ if roll.startswith('+'):
113
+ # Advantage indicator
114
+ roll = roll[1:]
115
+
116
+ self._adv = True
117
+ continue
118
+
119
+ if roll.startswith('-'):
120
+ # Disadvantage indicator
121
+ self._dis = True
122
+ roll = roll[1:]
123
+ continue
124
+
125
+ if roll.startswith('save'):
126
+ save = True
127
+ roll = roll[4:]
128
+ continue
129
+
130
+ if character is not None and len(roll) >= 3 and character.has_attr(roll[:3]):
131
+ attr = roll[:3]
132
+ roll = roll[3:]
133
+ continue
134
+
135
+ if roll.startswith(' '):
136
+ # Whitespace is ignored
137
+ roll = roll[1:]
138
+ continue
139
+
140
+ # If we didn't hit anything, then we never will, so we bail
141
+ raise RuntimeError(f'invalid syntax: unknown directive ({roll})')
142
+
143
+ if attr != '':
144
+ if save:
145
+ self._bonus += character.save_val(attr)
146
+ else:
147
+ self._bonus += character.abil_val(attr)
@@ -0,0 +1,78 @@
1
+ from dataclasses import dataclass
2
+ from typing import List
3
+
4
+ from .roll import Roll
5
+
6
+
7
+ def test_roll():
8
+ r = Roll()
9
+ r._dice.extend([2, 2, 2])
10
+ r1 = r.roll()
11
+
12
+ assert len(r1) == 3
13
+ assert r1[0] == 1 or r1[0] == 2
14
+ assert r1[1] == 1 or r1[1] == 2
15
+ assert r1[2] == 1 or r1[2] == 2
16
+
17
+
18
+ @dataclass
19
+ class FakeRoller:
20
+ seq: List[int]
21
+ index: int = -1
22
+
23
+ def roll(self, bound: int) -> int:
24
+ self.index += 1
25
+
26
+ value = self.seq[self.index]
27
+ if value >= bound:
28
+ raise RuntimeError(f"Value {value} exceeds bound {bound}")
29
+
30
+ return value
31
+
32
+
33
+ def test_roll_adv():
34
+ r = Roll(roller=FakeRoller([1, 2]).roll)
35
+ r._dice.extend([20])
36
+ r._adv = True
37
+ vs = r.roll()
38
+
39
+ assert len(vs) == 1
40
+ assert vs[0] == 2
41
+
42
+
43
+ def test_roll_dis():
44
+ r = Roll(roller=FakeRoller([2, 1]).roll)
45
+ r._dice.extend([20])
46
+ r._dis = True
47
+ vs = r.roll()
48
+
49
+ assert len(vs) == 1
50
+ assert vs[0] == 1
51
+
52
+
53
+ def test_parse_dice():
54
+ r = Roll()
55
+ r.parse('d6 d10 d20 d14')
56
+
57
+ assert r._dice == [6, 10, 20, 14]
58
+
59
+
60
+ def test_parse_count():
61
+ r = Roll()
62
+ r.parse('d6x3')
63
+
64
+ assert r._dice == [6, 6, 6]
65
+
66
+
67
+ def test_parse_adv():
68
+ r = Roll()
69
+ r.parse('d20+')
70
+
71
+ assert r._adv
72
+
73
+
74
+ def test_parse_dis():
75
+ r = Roll()
76
+ r.parse('d20-')
77
+
78
+ assert r._dis
@@ -0,0 +1,141 @@
1
+ from typing import List, Dict, Optional
2
+
3
+
4
+ class Theme:
5
+ _chars: str = 'HT0123456789='
6
+
7
+ def __init__(self, templ: str, widths: Optional[Dict[str, int]] = None, padding: int = 0, width: int = 0, space_width: int = 2):
8
+ self._templ: List[str] = templ.split('\n')[1:-1]
9
+
10
+ self._height = len(self._templ)
11
+ if self._height == 0:
12
+ raise RuntimeError('invalid theme: no template specified')
13
+
14
+ if widths is None:
15
+ self._widths = {}
16
+ else:
17
+ self._widths = widths
18
+
19
+ self._pad = padding
20
+ self._width = width
21
+ self._space_width = space_width
22
+
23
+ self._validate()
24
+
25
+ def render(self, s: str) -> str:
26
+ out_lines = [''] * self._height
27
+ out_str = ''
28
+
29
+ # A leading or trailing newline makes things weird
30
+ # and isn't necessary anyway, so drop them
31
+ in_str = s.strip('\n')
32
+
33
+ for c in in_str:
34
+ if c == '\n':
35
+ out_str += '\n'.join(out_lines) + '\n'
36
+ out_lines = [''] * self._height
37
+ continue
38
+
39
+ if c == ' ':
40
+ for line_index in range(self._height):
41
+ out_lines[line_index] += ' ' * self._space_width
42
+ continue
43
+
44
+ if c not in self._chars:
45
+ raise RuntimeError(f'invalid string: character {c} is not supported')
46
+
47
+ offset = self._get_offset(c)
48
+ width = self._get_width(c)
49
+ for line_index in range(self._height):
50
+ out_lines[line_index] += self._templ[line_index][offset:offset + width].ljust(width)
51
+
52
+ return out_str + '\n'.join(out_lines)
53
+
54
+ def _validate(self):
55
+ self.render(self._chars)
56
+
57
+ def _get_offset(self, c: str):
58
+ offset = 0
59
+ ci = self._chars.index(c)
60
+
61
+ # Account for everything before the character we want
62
+ for i in range(ci):
63
+ offset += self._get_width(self._chars[i])
64
+ offset += self._pad
65
+
66
+ return offset
67
+
68
+ def _get_width(self, c: str) -> int:
69
+ w = 0
70
+ if c in self._widths:
71
+ w = self._widths[c]
72
+
73
+ if w == 0:
74
+ if self._width == 0:
75
+ raise RuntimeError(f'invalid theme: no width specified for char {c}')
76
+ w = self._width
77
+
78
+ return w
79
+
80
+
81
+ DEFAULT = r'''
82
+ ▄▄ ▄▄ ▄▄▄▄▄▄▄▄ ▄▄▄▄ ▄▄▄ ▄▄▄▄▄ ▄▄▄▄▄ ▄▄▄ ▄▄▄▄▄▄▄ ▄▄▄▄ ▄▄▄▄▄▄▄▄ ▄▄▄▄ ▄▄▄▄
83
+ ██ ██ ▀▀▀██▀▀▀ ██▀▀██ █▀██ █▀▀▀▀██▄ █▀▀▀▀██▄ ▄███ ██▀▀▀▀▀ ██▀▀▀█ ▀▀▀▀▀███ ▄██▀▀██▄ ▄██▀▀██▄
84
+ ██ ██ ██ ██ ██ ██ ██ ▄██ █▀ ██ ██▄▄▄▄ ██ ▄▄▄ ▄██ ██▄ ▄██ ██ ██ ▄▄▄▄▄▄▄▄
85
+ ████████ ██ ██ ██ ██ ██ ▄█▀ █████ ▄█▀ ██ █▀▀▀▀██▄ ███▀▀██▄ ██ ██████ ▀██▄▄███ ▀▀▀▀▀▀▀▀
86
+ ██ ██ ██ ██ ██ ██ ▄█▀ ▀██ ████████ ██ ██ ██ ██ ██▀ ▀██ ▀▀▀ ██ ▄▄▄▄▄▄▄▄
87
+ ██ ██ ██ ██▄▄██ ▄▄▄██▄▄▄ ▄██▄▄▄▄▄ █▄▄▄▄██▀ ██ █▄▄▄▄██▀ ▀██▄▄██▀ ██ ▀██▄▄██▀ █▄▄▄██ ▀▀▀▀▀▀▀▀
88
+ ▀▀ ▀▀ ▀▀ ▀▀▀▀ ▀▀▀▀▀▀▀▀ ▀▀▀▀▀▀▀▀ ▀▀▀▀▀ ▀▀ ▀▀▀▀▀ ▀▀▀▀ ▀▀ ▀▀▀▀ ▀▀▀▀
89
+ '''
90
+
91
+ ALPHA = r'''
92
+ H H TTTTTT 000 11 22 333 4 4 5555 6 77777 888 9999
93
+ H H TT 0 00 111 2 2 3 4 4 5 6 7 8 8 9 9 ===
94
+ HHHH TT 0 0 0 11 2 33 4444 555 6666 7 888 9999
95
+ H H TT 00 0 11 2 3 4 5 6 6 7 8 8 9 ===
96
+ H H TT 000 1111 2222 333 4 555 666 7 888 9
97
+ '''
98
+
99
+ OUTLINE = r'''
100
+ _ _ _______ ___ __ ___ ____ _ _ _____ __ ______ ___ ___
101
+ | | | | |__ __| / _ \ /_ | |__ \ |___ \ | || | | ____| / / |____ | / _ \ / _ \ ______
102
+ | |__| | | | | | | | | | ) | __) | | || |_ | |__ / /_ / / | (_) | | (_) | |______|
103
+ | __ | | | | | | | | | / / |__ < |__ _| |___ \ | '_ \ / / > _ < \__, | ______
104
+ | | | | | | | |_| | | | / /_ ___) | | | ___) | | (_) | / / | (_) | / / |______|
105
+ |_| |_| |_| \___/ |_| |____| |____/ |_| |____/ \___/ /_/ \___/ /_/
106
+ '''
107
+
108
+
109
+ ALL_THEMES: Dict[str, Theme] = {
110
+ 'default': Theme(DEFAULT, width=10, space_width=8),
111
+ 'alpha': Theme(ALPHA, {
112
+ 'T': 8,
113
+ '0': 7,
114
+ '6': 7,
115
+ '7': 7,
116
+ '8': 7,
117
+ '9': 7,
118
+ '=': 5,
119
+ }, width=6, padding=3, space_width=5),
120
+ 'outline': Theme(OUTLINE, {
121
+ 'H': 8,
122
+ 'T': 9,
123
+ '1': 4,
124
+ '2': 6,
125
+ '4': 8,
126
+ '7': 8,
127
+ '=': 8,
128
+ }, width=7, padding=1, space_width=3)
129
+ }
130
+
131
+
132
+ def get_themes() -> List[str]:
133
+ return list(ALL_THEMES.keys())
134
+
135
+
136
+ def get_theme(name: str) -> Theme:
137
+ key = name.strip().lower()
138
+ if key in ALL_THEMES:
139
+ return ALL_THEMES[key]
140
+
141
+ raise RuntimeError(f'invalid theme name: {name}')
@@ -0,0 +1,61 @@
1
+ from .themes import Theme
2
+
3
+
4
+ TEST_THEME = r'''
5
+ H T 0 1 2 3 4 5 6 7 8 9 =
6
+ H T 0 1 2 3 4 5 6 7 8 9 =
7
+ '''
8
+
9
+
10
+ def test_first_letter():
11
+ t = Theme(TEST_THEME, width=1, padding=1)
12
+ assert t.render('H') == 'H\nH'
13
+
14
+
15
+ def test_last_letter():
16
+ t = Theme(TEST_THEME, width=1, padding=1)
17
+ assert t.render('=') == '=\n='
18
+
19
+
20
+ def test_middle_letter():
21
+ t = Theme(TEST_THEME, width=1, padding=1)
22
+ assert t.render('5') == '5\n5'
23
+
24
+
25
+ def test_space():
26
+ t = Theme(TEST_THEME, width=1, padding=1, space_width=1)
27
+ assert t.render(' ') == ' \n '
28
+
29
+
30
+ def test_multiple_letters():
31
+ t = Theme(TEST_THEME, width=1, padding=1, space_width=2)
32
+ assert t.render('H T') == 'H T\nH T'
33
+
34
+
35
+ TEST_THEME_WIDE = r'''
36
+ HH TT 00 11 22 33 44 55 66 77 88 99 ==
37
+ HH TT 00 11 22 33 44 55 66 77 88 99 ==
38
+ '''
39
+
40
+
41
+ def test_wide_theme():
42
+ t = Theme(TEST_THEME_WIDE, width=2, padding=1, space_width=3)
43
+ assert t.render('H') == 'HH\nHH'
44
+ assert t.render('=') == '==\n=='
45
+ assert t.render('5') == '55\n55'
46
+ assert t.render(' ') == ' \n '
47
+ assert t.render('H T') == 'HH TT\nHH TT'
48
+
49
+
50
+ TEST_THEME_VARIED = r'''
51
+ HHH TT 00 11 22 33 44 555 66 77 88 99 ===
52
+ HHH TT 00 11 22 33 44 555 66 77 88 99 ===
53
+ '''
54
+
55
+
56
+ def test_wide_varied():
57
+ t = Theme(TEST_THEME_VARIED, widths={'H': 3, '5': 3, '=': 3}, width=2, padding=1)
58
+ assert t.render('H') == 'HHH\nHHH'
59
+ assert t.render('=') == '===\n==='
60
+ assert t.render('5') == '555\n555'
61
+ assert t.render('H T') == 'HHH TT\nHHH TT'