cli-tamagotchi 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.
Files changed (32) hide show
  1. cli_tamagotchi-1.0.0/LICENSE +21 -0
  2. cli_tamagotchi-1.0.0/PKG-INFO +119 -0
  3. cli_tamagotchi-1.0.0/README.md +86 -0
  4. cli_tamagotchi-1.0.0/plugins/claude_code/__init__.py +3 -0
  5. cli_tamagotchi-1.0.0/plugins/claude_code/plugin.py +176 -0
  6. cli_tamagotchi-1.0.0/pyproject.toml +69 -0
  7. cli_tamagotchi-1.0.0/setup.cfg +4 -0
  8. cli_tamagotchi-1.0.0/setup.py +5 -0
  9. cli_tamagotchi-1.0.0/src/cli_tamagotchi/__init__.py +5 -0
  10. cli_tamagotchi-1.0.0/src/cli_tamagotchi/__main__.py +5 -0
  11. cli_tamagotchi-1.0.0/src/cli_tamagotchi/characters.py +104 -0
  12. cli_tamagotchi-1.0.0/src/cli_tamagotchi/cli.py +1049 -0
  13. cli_tamagotchi-1.0.0/src/cli_tamagotchi/engine.py +453 -0
  14. cli_tamagotchi-1.0.0/src/cli_tamagotchi/graveyard.py +79 -0
  15. cli_tamagotchi-1.0.0/src/cli_tamagotchi/illnesses.py +134 -0
  16. cli_tamagotchi-1.0.0/src/cli_tamagotchi/models.py +238 -0
  17. cli_tamagotchi-1.0.0/src/cli_tamagotchi/plugins/__init__.py +17 -0
  18. cli_tamagotchi-1.0.0/src/cli_tamagotchi/plugins/base.py +54 -0
  19. cli_tamagotchi-1.0.0/src/cli_tamagotchi/plugins/hooks.py +49 -0
  20. cli_tamagotchi-1.0.0/src/cli_tamagotchi/plugins/manager.py +184 -0
  21. cli_tamagotchi-1.0.0/src/cli_tamagotchi/render.py +551 -0
  22. cli_tamagotchi-1.0.0/src/cli_tamagotchi/sprites/__init__.py +3 -0
  23. cli_tamagotchi-1.0.0/src/cli_tamagotchi/sprites/ascii.py +1676 -0
  24. cli_tamagotchi-1.0.0/src/cli_tamagotchi/storage.py +50 -0
  25. cli_tamagotchi-1.0.0/src/cli_tamagotchi.egg-info/PKG-INFO +119 -0
  26. cli_tamagotchi-1.0.0/src/cli_tamagotchi.egg-info/SOURCES.txt +30 -0
  27. cli_tamagotchi-1.0.0/src/cli_tamagotchi.egg-info/dependency_links.txt +1 -0
  28. cli_tamagotchi-1.0.0/src/cli_tamagotchi.egg-info/entry_points.txt +9 -0
  29. cli_tamagotchi-1.0.0/src/cli_tamagotchi.egg-info/requires.txt +10 -0
  30. cli_tamagotchi-1.0.0/src/cli_tamagotchi.egg-info/top_level.txt +2 -0
  31. cli_tamagotchi-1.0.0/tests/test_cli_tamagotchi.py +766 -0
  32. cli_tamagotchi-1.0.0/tests/test_plugins.py +150 -0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Eneko Galan
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.
@@ -0,0 +1,119 @@
1
+ Metadata-Version: 2.4
2
+ Name: cli-tamagotchi
3
+ Version: 1.0.0
4
+ Summary: A terminal Tamagotchi that reacts to your care.
5
+ Author: Eneko Galan Elorza
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/enegalan/cli-tamagotchi
8
+ Project-URL: Repository, https://github.com/enegalan/cli-tamagotchi.git
9
+ Project-URL: Issues, https://github.com/enegalan/cli-tamagotchi/issues
10
+ Project-URL: Changelog, https://github.com/enegalan/cli-tamagotchi/releases
11
+ Keywords: cli,game,tamagotchi,terminal
12
+ Classifier: Development Status :: 4 - Beta
13
+ Classifier: Environment :: Console
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3 :: Only
16
+ Classifier: Programming Language :: Python :: 3.9
17
+ Classifier: Programming Language :: Python :: 3.10
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Programming Language :: Python :: 3.13
21
+ Classifier: Topic :: Games/Entertainment
22
+ Requires-Python: >=3.9
23
+ Description-Content-Type: text/markdown
24
+ License-File: LICENSE
25
+ Requires-Dist: rich
26
+ Provides-Extra: claude-code
27
+ Provides-Extra: agents
28
+ Provides-Extra: dev
29
+ Requires-Dist: build>=1.2; extra == "dev"
30
+ Requires-Dist: twine>=5; extra == "dev"
31
+ Requires-Dist: pytest>=7; extra == "dev"
32
+ Dynamic: license-file
33
+
34
+ # cli-tamagotchi
35
+
36
+ `cli-tamagotchi` is a terminal-first Tamagotchi. It gives you one persistent pet, keeps aging it while the CLI is closed, and lets you care for it with a small `tama` command set.
37
+
38
+ ## Features
39
+
40
+ - **One active pet** in `~/.cli-tamagotchi/pet.json` (override the data directory with `CLI_TAMAGOTCHI_HOME`)
41
+ - **Graveyard** for past pets in `~/.cli-tamagotchi/graveyard.json`
42
+ - **Stats:** hunger, happiness, health, weight, energy, dirtiness; wake/sleep; optional illnesses
43
+ - **Life stages:** Egg → Baby → Child → Adult (time-based growth), with death when care or health fails
44
+ - **Offline decay** reconciled from elapsed real time whenever you run a command
45
+ - **Care actions:** feed, play, lights on/off, clean, medicine (one-hour cooldown; helps cure illness)
46
+ - **Event log** stored with the pet state (trimmed to the most recent entries)
47
+ - **ASCII sprites** with mood and stage-aware animation in the UI
48
+ - **CLI subcommands** for quick actions, plus **`tama` alone** for the interactive loop
49
+ - **Plugin system** with lifecycle hooks (`on_tick`, `on_event`, `on_action`, `on_external_event`, ...)
50
+
51
+ ## Requirements
52
+
53
+ - Python **3.9+**
54
+
55
+ ## Install and run
56
+
57
+ ```bash
58
+ pip install cli-tamagotchi
59
+ ```
60
+
61
+ Editable install from a clone:
62
+
63
+ ```bash
64
+ python3 -m venv .venv
65
+ source .venv/bin/activate
66
+ pip install -e .
67
+ ```
68
+
69
+ ### CLI (`tama`)
70
+
71
+ ```bash
72
+ tama status
73
+ tama feed
74
+ tama play
75
+ tama lights
76
+ tama clean
77
+ tama medicine
78
+ tama logs
79
+ tama new # only when no living pet
80
+ tama graveyard
81
+ tama plugin emit my_event --data '{"k":"v"}' # notify all plugins
82
+ tama # interactive UI
83
+ ```
84
+
85
+ Plugins are loaded when `tama` starts. To pick up plugin code changes, restart the command.
86
+
87
+ Setup details: [integrations/README.md](integrations/README.md).
88
+
89
+ Run without installing the console script:
90
+
91
+ ```bash
92
+ PYTHONPATH=src:plugins python3 -m cli_tamagotchi status
93
+ ```
94
+
95
+ Use `tama -h` for built-in help on subcommands.
96
+
97
+ ## Interactive mode
98
+
99
+ Running `tama` with no arguments starts the main UI: status, animated pet, stat bars, and an action grid. On a proper TTY, navigation uses single-key input; otherwise a simple line-based fallback is used.
100
+
101
+ While your pet is alive you can use feed, play, lights, clean, medicine, open the **graveyard** view, or quit. After death you can start a **new pet** (when allowed) or browse the graveyard. The event log can be scrolled when shown in the interactive layout.
102
+
103
+ ## Persistence
104
+
105
+ State lives under `~/.cli-tamagotchi/` by default (or under `CLI_TAMAGOTCHI_HOME` if set). The save includes name, character, stage, weight, stats, sleep and dirtiness, illness and medicine timestamps, timestamps for creation/updates/ticks/interactions, and the recent event log. Offline progression runs from the last processed tick when any command loads the pet.
106
+
107
+ ## Tests
108
+
109
+ ```bash
110
+ python3 -m pytest tests/
111
+ ```
112
+
113
+ ## Contributing
114
+
115
+ See [CONTRIBUTING.md](CONTRIBUTING.md).
116
+
117
+ ## License
118
+
119
+ MIT — see [LICENSE](LICENSE).
@@ -0,0 +1,86 @@
1
+ # cli-tamagotchi
2
+
3
+ `cli-tamagotchi` is a terminal-first Tamagotchi. It gives you one persistent pet, keeps aging it while the CLI is closed, and lets you care for it with a small `tama` command set.
4
+
5
+ ## Features
6
+
7
+ - **One active pet** in `~/.cli-tamagotchi/pet.json` (override the data directory with `CLI_TAMAGOTCHI_HOME`)
8
+ - **Graveyard** for past pets in `~/.cli-tamagotchi/graveyard.json`
9
+ - **Stats:** hunger, happiness, health, weight, energy, dirtiness; wake/sleep; optional illnesses
10
+ - **Life stages:** Egg → Baby → Child → Adult (time-based growth), with death when care or health fails
11
+ - **Offline decay** reconciled from elapsed real time whenever you run a command
12
+ - **Care actions:** feed, play, lights on/off, clean, medicine (one-hour cooldown; helps cure illness)
13
+ - **Event log** stored with the pet state (trimmed to the most recent entries)
14
+ - **ASCII sprites** with mood and stage-aware animation in the UI
15
+ - **CLI subcommands** for quick actions, plus **`tama` alone** for the interactive loop
16
+ - **Plugin system** with lifecycle hooks (`on_tick`, `on_event`, `on_action`, `on_external_event`, ...)
17
+
18
+ ## Requirements
19
+
20
+ - Python **3.9+**
21
+
22
+ ## Install and run
23
+
24
+ ```bash
25
+ pip install cli-tamagotchi
26
+ ```
27
+
28
+ Editable install from a clone:
29
+
30
+ ```bash
31
+ python3 -m venv .venv
32
+ source .venv/bin/activate
33
+ pip install -e .
34
+ ```
35
+
36
+ ### CLI (`tama`)
37
+
38
+ ```bash
39
+ tama status
40
+ tama feed
41
+ tama play
42
+ tama lights
43
+ tama clean
44
+ tama medicine
45
+ tama logs
46
+ tama new # only when no living pet
47
+ tama graveyard
48
+ tama plugin emit my_event --data '{"k":"v"}' # notify all plugins
49
+ tama # interactive UI
50
+ ```
51
+
52
+ Plugins are loaded when `tama` starts. To pick up plugin code changes, restart the command.
53
+
54
+ Setup details: [integrations/README.md](integrations/README.md).
55
+
56
+ Run without installing the console script:
57
+
58
+ ```bash
59
+ PYTHONPATH=src:plugins python3 -m cli_tamagotchi status
60
+ ```
61
+
62
+ Use `tama -h` for built-in help on subcommands.
63
+
64
+ ## Interactive mode
65
+
66
+ Running `tama` with no arguments starts the main UI: status, animated pet, stat bars, and an action grid. On a proper TTY, navigation uses single-key input; otherwise a simple line-based fallback is used.
67
+
68
+ While your pet is alive you can use feed, play, lights, clean, medicine, open the **graveyard** view, or quit. After death you can start a **new pet** (when allowed) or browse the graveyard. The event log can be scrolled when shown in the interactive layout.
69
+
70
+ ## Persistence
71
+
72
+ State lives under `~/.cli-tamagotchi/` by default (or under `CLI_TAMAGOTCHI_HOME` if set). The save includes name, character, stage, weight, stats, sleep and dirtiness, illness and medicine timestamps, timestamps for creation/updates/ticks/interactions, and the recent event log. Offline progression runs from the last processed tick when any command loads the pet.
73
+
74
+ ## Tests
75
+
76
+ ```bash
77
+ python3 -m pytest tests/
78
+ ```
79
+
80
+ ## Contributing
81
+
82
+ See [CONTRIBUTING.md](CONTRIBUTING.md).
83
+
84
+ ## License
85
+
86
+ MIT — see [LICENSE](LICENSE).
@@ -0,0 +1,3 @@
1
+ from .plugin import ClaudeCodePlugin
2
+
3
+ __all__ = ("ClaudeCodePlugin",)
@@ -0,0 +1,176 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import time
5
+ from collections import Counter, deque
6
+ from datetime import datetime
7
+ from pathlib import Path
8
+ from typing import Any
9
+
10
+ from cli_tamagotchi.models import PetState, clamp_stat
11
+ from cli_tamagotchi.plugins.base import BasePlugin
12
+
13
+
14
+ class AgentBehaviorClassifier:
15
+ WINDOW = 20
16
+
17
+ def __init__(self) -> None:
18
+ self._tool_calls: deque[dict[str, Any]] = deque(maxlen=self.WINDOW)
19
+ self._last_write_at: float = 0.0
20
+ self._error_count = 0
21
+ self._loop_counter: Counter[str] = Counter()
22
+
23
+ def record_tool_call(self, tool: str, exit_code: int = 0) -> str:
24
+ now = time.time()
25
+ self._tool_calls.append({"tool": tool, "ts": now, "exit": exit_code})
26
+ self._loop_counter[tool] += 1
27
+
28
+ if tool in ("write_file", "edit_file", "bash") and exit_code == 0:
29
+ self._last_write_at = now
30
+ self._loop_counter.clear()
31
+
32
+ if exit_code != 0:
33
+ self._error_count += 1
34
+ else:
35
+ self._error_count = max(0, self._error_count - 1)
36
+
37
+ return self.classify()
38
+
39
+ def classify(self) -> str:
40
+ if self._error_count >= 5:
41
+ return "BLOCKED"
42
+ if any(count >= 5 for count in self._loop_counter.values()):
43
+ return "LOOPING"
44
+ recent_tools = [e["tool"] for e in self._tool_calls]
45
+ write_tools = {"write_file", "edit_file", "bash", "str_replace_editor"}
46
+ read_tools = {"read_file", "list_dir", "grep", "glob"}
47
+ writes = sum(1 for t in recent_tools if t in write_tools)
48
+ reads = sum(1 for t in recent_tools if t in read_tools)
49
+ if writes > reads:
50
+ return "SHIPPING"
51
+ if reads > 5 and writes == 0:
52
+ return "EXPLORING"
53
+ return "WORKING"
54
+
55
+
56
+ BEHAVIOR_REACTIONS: dict[str, dict[str, int]] = {
57
+ "SHIPPING": {"happy_delta": 1, "hunger_delta": 0},
58
+ "LOOPING": {"happy_delta": -1, "hunger_delta": -1},
59
+ "EXPLORING": {"happy_delta": 0, "hunger_delta": 0},
60
+ "BLOCKED": {"happy_delta": -1, "hunger_delta": -1},
61
+ "WORKING": {"happy_delta": 0, "hunger_delta": 0},
62
+ "DONE_SUCCESS": {"happy_delta": 2, "hunger_delta": 1},
63
+ "DONE_FAILURE": {"happy_delta": -1, "hunger_delta": -1},
64
+ "TESTS_PASSED": {"happy_delta": 2, "hunger_delta": 1},
65
+ "TESTS_FAILED": {"happy_delta": -1, "hunger_delta": 0},
66
+ }
67
+
68
+
69
+ def _apply_reaction(pet_state: PetState, key: str) -> None:
70
+ reaction = BEHAVIOR_REACTIONS.get(key, BEHAVIOR_REACTIONS["WORKING"])
71
+ pet_state.happiness = clamp_stat(pet_state.happiness + reaction["happy_delta"] * 15)
72
+ pet_state.hunger = clamp_stat(pet_state.hunger + reaction["hunger_delta"] * 12)
73
+
74
+
75
+ class ClaudeCodePlugin(BasePlugin):
76
+ name = "claude_code"
77
+ description = "Reacts to Claude Code agent behavior"
78
+ version = "0.1.0"
79
+ events_jsonl_basename = "claude_events.jsonl"
80
+
81
+ def __init__(self) -> None:
82
+ self._classifier = AgentBehaviorClassifier()
83
+ self._last_event_line = 0
84
+ self._last_behavior = "WORKING"
85
+ self._behavior_tick = 0
86
+
87
+ def _event_path(self) -> Path:
88
+ path = self.events_jsonl_path()
89
+ if path is None:
90
+ raise RuntimeError("ClaudeCodePlugin.events_jsonl_basename must be set")
91
+ return path
92
+
93
+ def on_load(self) -> None:
94
+ path = self._event_path()
95
+ path.parent.mkdir(parents=True, exist_ok=True)
96
+ if not path.exists():
97
+ path.touch()
98
+
99
+ def on_tick(self, pet_state: PetState, tick_time: datetime) -> None:
100
+ self._poll_events(pet_state)
101
+ self._behavior_tick += 1
102
+ if self._behavior_tick % 60 == 0:
103
+ _apply_reaction(pet_state, self._last_behavior)
104
+
105
+ def _poll_events(self, pet_state: PetState) -> None:
106
+ path = self._event_path()
107
+ if not path.exists():
108
+ return
109
+ lines = path.read_text(encoding="utf-8").splitlines()
110
+ new_lines = lines[self._last_event_line :]
111
+ self._last_event_line = len(lines)
112
+
113
+ for line in new_lines:
114
+ try:
115
+ event = json.loads(line)
116
+ self._process_event(pet_state, event)
117
+ except (json.JSONDecodeError, TypeError, ValueError, KeyError):
118
+ pass
119
+
120
+ def _process_event(self, pet_state: PetState, event: dict[str, Any]) -> None:
121
+ etype = event.get("type", "")
122
+ if etype == "pre_tool":
123
+ return
124
+ if etype == "post_tool":
125
+ tool = event.get("tool", "unknown")
126
+ exit_code = int(event.get("exit_code", 0))
127
+ behavior = self._classifier.record_tool_call(tool, exit_code)
128
+ self._last_behavior = behavior
129
+ if tool == "bash" and "pytest" in event.get("command", ""):
130
+ if exit_code == 0:
131
+ _apply_reaction(pet_state, "TESTS_PASSED")
132
+ else:
133
+ _apply_reaction(pet_state, "TESTS_FAILED")
134
+ elif etype == "stop":
135
+ reason = event.get("reason", "")
136
+ if reason in ("task_complete", "success"):
137
+ _apply_reaction(pet_state, "DONE_SUCCESS")
138
+ else:
139
+ _apply_reaction(pet_state, "DONE_FAILURE")
140
+ elif etype == "subagent_start":
141
+ pet_state.happiness = clamp_stat(pet_state.happiness - 12)
142
+
143
+ def on_external_event(self, event_type: str, data: dict[str, Any]) -> None:
144
+ pass
145
+
146
+
147
+ def build_hook_event(args: list[str]) -> tuple[Path, dict[str, object]] | None:
148
+ """Parse ``tama-hook`` argv for Claude Code; return None if these are not Claude commands."""
149
+ if not args:
150
+ return None
151
+
152
+ payload: dict[str, object] = {"ts": time.time()}
153
+ command = args[0]
154
+
155
+ if command == "pre-tool" and len(args) >= 2:
156
+ payload.update({"type": "pre_tool", "tool": args[1]})
157
+ elif command == "post-tool" and len(args) >= 3:
158
+ payload.update(
159
+ {
160
+ "type": "post_tool",
161
+ "tool": args[1],
162
+ "exit_code": int(args[2]),
163
+ "command": " ".join(args[3:]) if len(args) > 3 else "",
164
+ }
165
+ )
166
+ elif command == "stop" and len(args) >= 2:
167
+ payload.update({"type": "stop", "reason": args[1]})
168
+ elif command == "subagent-start":
169
+ payload.update({"type": "subagent_start"})
170
+ else:
171
+ return None
172
+
173
+ path = ClaudeCodePlugin.events_jsonl_path()
174
+ if path is None:
175
+ return None
176
+ return (path, payload)
@@ -0,0 +1,69 @@
1
+ [build-system]
2
+ requires = ["setuptools>=77"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "cli-tamagotchi"
7
+ version = "1.0.0"
8
+ description = "A terminal Tamagotchi that reacts to your care."
9
+ readme = "README.md"
10
+ requires-python = ">=3.9"
11
+ authors = [
12
+ { name = "Eneko Galan Elorza" }
13
+ ]
14
+ license = "MIT"
15
+ license-files = ["LICENSE"]
16
+ keywords = ["cli", "game", "tamagotchi", "terminal"]
17
+ classifiers = [
18
+ "Development Status :: 4 - Beta",
19
+ "Environment :: Console",
20
+ "Programming Language :: Python :: 3",
21
+ "Programming Language :: Python :: 3 :: Only",
22
+ "Programming Language :: Python :: 3.9",
23
+ "Programming Language :: Python :: 3.10",
24
+ "Programming Language :: Python :: 3.11",
25
+ "Programming Language :: Python :: 3.12",
26
+ "Programming Language :: Python :: 3.13",
27
+ "Topic :: Games/Entertainment",
28
+ ]
29
+ dependencies = [
30
+ "rich",
31
+ ]
32
+
33
+ [project.urls]
34
+ Homepage = "https://github.com/enegalan/cli-tamagotchi"
35
+ Repository = "https://github.com/enegalan/cli-tamagotchi.git"
36
+ Issues = "https://github.com/enegalan/cli-tamagotchi/issues"
37
+ Changelog = "https://github.com/enegalan/cli-tamagotchi/releases"
38
+
39
+ [project.optional-dependencies]
40
+ claude-code = []
41
+ agents = []
42
+ dev = [
43
+ "build>=1.2",
44
+ "twine>=5",
45
+ "pytest>=7",
46
+ ]
47
+
48
+ [project.scripts]
49
+ tama = "cli_tamagotchi.cli:main"
50
+ tama-hook = "cli_tamagotchi.plugins.hooks:tama_hook_main"
51
+
52
+ [project.entry-points."cli_tamagotchi.plugins"]
53
+ claude_code = "claude_code.plugin:ClaudeCodePlugin"
54
+
55
+ [project.entry-points."cli_tamagotchi.hook_builders"]
56
+ claude_code = "claude_code.plugin:build_hook_event"
57
+
58
+ [tool.setuptools]
59
+ package-dir = {
60
+ "" = "src",
61
+ "claude_code" = "plugins/claude_code",
62
+ }
63
+
64
+ [tool.setuptools.packages.find]
65
+ where = ["src", "plugins"]
66
+
67
+ [tool.pytest.ini_options]
68
+ pythonpath = ["src", "plugins"]
69
+ testpaths = ["tests"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,5 @@
1
+ """Compatibility shim for editable installs with older pip versions."""
2
+
3
+ from setuptools import setup
4
+
5
+ setup()
@@ -0,0 +1,5 @@
1
+ """cli-tamagotchi package."""
2
+
3
+ from .cli import main
4
+
5
+ __all__ = ["main"]
@@ -0,0 +1,5 @@
1
+ from .cli import main
2
+
3
+
4
+ if __name__ == "__main__":
5
+ raise SystemExit(main())
@@ -0,0 +1,104 @@
1
+ from __future__ import annotations
2
+
3
+ import random
4
+ from dataclasses import dataclass
5
+ from enum import Enum
6
+
7
+ FALLBACK_CHARACTER = "Cat"
8
+
9
+
10
+ class Rarity(str, Enum):
11
+ COMMON = "common"
12
+ UNCOMMON = "uncommon"
13
+ RARE = "rare"
14
+ EPIC = "epic"
15
+ LEGENDARY = "legendary"
16
+
17
+
18
+ RARITY_DISPLAY = {
19
+ Rarity.COMMON: "Common",
20
+ Rarity.UNCOMMON: "Uncommon",
21
+ Rarity.RARE: "Rare",
22
+ Rarity.EPIC: "Epic",
23
+ Rarity.LEGENDARY: "Legendary",
24
+ }
25
+
26
+
27
+ @dataclass(frozen=True)
28
+ class CharacterSpec:
29
+ character_id: str
30
+ rarity: Rarity
31
+ roll_weight: int
32
+ rich_style: str
33
+ healthy_weight_min: int
34
+ healthy_weight_max: int
35
+
36
+
37
+ CHARACTER_POOL: tuple[CharacterSpec, ...] = (
38
+ CharacterSpec(
39
+ character_id="Cat",
40
+ rarity=Rarity.COMMON,
41
+ roll_weight=48,
42
+ rich_style="bright_cyan",
43
+ healthy_weight_min=3,
44
+ healthy_weight_max=26,
45
+ ),
46
+ CharacterSpec(
47
+ character_id="Dog",
48
+ rarity=Rarity.UNCOMMON,
49
+ roll_weight=25,
50
+ rich_style="bright_yellow",
51
+ healthy_weight_min=3,
52
+ healthy_weight_max=24,
53
+ ),
54
+ CharacterSpec(
55
+ character_id="Fox",
56
+ rarity=Rarity.RARE,
57
+ roll_weight=17,
58
+ rich_style="bright_magenta",
59
+ healthy_weight_min=2,
60
+ healthy_weight_max=20,
61
+ ),
62
+ CharacterSpec(
63
+ character_id="Owl",
64
+ rarity=Rarity.EPIC,
65
+ roll_weight=10,
66
+ rich_style="bright_blue",
67
+ healthy_weight_min=2,
68
+ healthy_weight_max=18,
69
+ ),
70
+ )
71
+
72
+ _CHARACTER_RARITY: dict[str, Rarity] = {spec.character_id: spec.rarity for spec in CHARACTER_POOL}
73
+ _CHARACTER_WEIGHTS: tuple[tuple[str, int], ...] = tuple(
74
+ (spec.character_id, spec.roll_weight) for spec in CHARACTER_POOL
75
+ )
76
+
77
+ CHARACTER_STYLE_BY_NAME: dict[str, str] = {spec.character_id: spec.rich_style for spec in CHARACTER_POOL}
78
+
79
+
80
+ def healthy_weight_bounds(character_id: str) -> tuple[int, int]:
81
+ for spec in CHARACTER_POOL:
82
+ if spec.character_id == character_id:
83
+ return (spec.healthy_weight_min, spec.healthy_weight_max)
84
+ return (3, 26)
85
+
86
+
87
+ def rarity_for_character(character_id: str) -> Rarity:
88
+ return _CHARACTER_RARITY.get(character_id, Rarity.COMMON)
89
+
90
+
91
+ def rarity_display_for_character(character_id: str) -> str:
92
+ return RARITY_DISPLAY[rarity_for_character(character_id)]
93
+
94
+
95
+ def character_status_label(character_id: str) -> str:
96
+ return f"{character_id} ({rarity_display_for_character(character_id)})"
97
+
98
+
99
+ def roll_starting_character(rng: random.Random | None = None) -> str:
100
+ random_source = rng if rng is not None else random.Random()
101
+ character_ids = [pair[0] for pair in _CHARACTER_WEIGHTS]
102
+ weights = [pair[1] for pair in _CHARACTER_WEIGHTS]
103
+ chosen = random_source.choices(character_ids, weights=weights, k=1)[0]
104
+ return chosen