gitquest 0.1.0__tar.gz → 0.2.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 (29) hide show
  1. {gitquest-0.1.0 → gitquest-0.2.0}/PKG-INFO +21 -3
  2. {gitquest-0.1.0 → gitquest-0.2.0}/README.md +20 -2
  3. {gitquest-0.1.0 → gitquest-0.2.0}/gitquest/__init__.py +1 -1
  4. {gitquest-0.1.0 → gitquest-0.2.0}/gitquest/cli.py +43 -16
  5. gitquest-0.2.0/gitquest/demo.py +79 -0
  6. {gitquest-0.1.0 → gitquest-0.2.0}/gitquest/generator.py +15 -10
  7. {gitquest-0.1.0 → gitquest-0.2.0}/gitquest.egg-info/PKG-INFO +21 -3
  8. {gitquest-0.1.0 → gitquest-0.2.0}/gitquest.egg-info/SOURCES.txt +2 -0
  9. {gitquest-0.1.0 → gitquest-0.2.0}/pyproject.toml +1 -1
  10. gitquest-0.2.0/tests/test_demo.py +51 -0
  11. {gitquest-0.1.0 → gitquest-0.2.0}/LICENSE +0 -0
  12. {gitquest-0.1.0 → gitquest-0.2.0}/gitquest/__main__.py +0 -0
  13. {gitquest-0.1.0 → gitquest-0.2.0}/gitquest/combat.py +0 -0
  14. {gitquest-0.1.0 → gitquest-0.2.0}/gitquest/data/items.json +0 -0
  15. {gitquest-0.1.0 → gitquest-0.2.0}/gitquest/data/monsters.json +0 -0
  16. {gitquest-0.1.0 → gitquest-0.2.0}/gitquest/data/ranks.json +0 -0
  17. {gitquest-0.1.0 → gitquest-0.2.0}/gitquest/engine.py +0 -0
  18. {gitquest-0.1.0 → gitquest-0.2.0}/gitquest/entities.py +0 -0
  19. {gitquest-0.1.0 → gitquest-0.2.0}/gitquest/flexcard.py +0 -0
  20. {gitquest-0.1.0 → gitquest-0.2.0}/gitquest/git_parser.py +0 -0
  21. {gitquest-0.1.0 → gitquest-0.2.0}/gitquest/renderer.py +0 -0
  22. {gitquest-0.1.0 → gitquest-0.2.0}/gitquest.egg-info/dependency_links.txt +0 -0
  23. {gitquest-0.1.0 → gitquest-0.2.0}/gitquest.egg-info/entry_points.txt +0 -0
  24. {gitquest-0.1.0 → gitquest-0.2.0}/gitquest.egg-info/requires.txt +0 -0
  25. {gitquest-0.1.0 → gitquest-0.2.0}/gitquest.egg-info/top_level.txt +0 -0
  26. {gitquest-0.1.0 → gitquest-0.2.0}/setup.cfg +0 -0
  27. {gitquest-0.1.0 → gitquest-0.2.0}/tests/test_combat.py +0 -0
  28. {gitquest-0.1.0 → gitquest-0.2.0}/tests/test_generator.py +0 -0
  29. {gitquest-0.1.0 → gitquest-0.2.0}/tests/test_git_parser.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: gitquest
3
- Version: 0.1.0
3
+ Version: 0.2.0
4
4
  Summary: Turn your git commit history into a playable ASCII dungeon roguelike.
5
5
  Author-email: jithin-jz <jithinjzx@gmail.com>
6
6
  License-Expression: MIT
@@ -126,13 +126,30 @@ pip install .
126
126
 
127
127
  ## 🎮 Play
128
128
 
129
- Run it inside any git repository no arguments needed:
129
+ Two commands works on any machine, even with no repo around:
130
130
 
131
131
  ```bash
132
+ pip install gitquest
132
133
  gitquest
133
134
  ```
134
135
 
135
- `python -m gitquest` works too.
136
+ If you run `gitquest` **inside a git repository**, it builds the dungeon from
137
+ *your* commit history. If you run it anywhere else, it automatically launches a
138
+ built-in **demo dungeon** so you can play right away.
139
+
140
+ `python -m gitquest` works too. Want your own repo? `cd` into it first, or pass
141
+ `--path`:
142
+
143
+ ```bash
144
+ cd path/to/your/repo && gitquest
145
+ gitquest --path ../some-other-repo
146
+ gitquest --demo # force the built-in demo dungeon
147
+ ```
148
+
149
+ ### Combat controls
150
+
151
+ During a fight, type one key + Enter: `a` attack · `d` defend · `i` item ·
152
+ `r` run.
136
153
 
137
154
  ### Flags
138
155
 
@@ -140,6 +157,7 @@ gitquest
140
157
  | ---------------- | --------------------------------------------------------- |
141
158
  | `--path <repo>` | Play on another repository (default: current directory). |
142
159
  | `--seed <n>` | Force a specific dungeon seed (default: derived from repo).|
160
+ | `--demo` | Play the built-in demo dungeon (no git repo required). |
143
161
  | `--fast` | Skip animations and "press Enter" pauses. |
144
162
  | `--stats-only` | Skip gameplay and print just the flex card. |
145
163
  | `--max-commits` | Cap/sample commits for huge repos (default: 300). |
@@ -98,13 +98,30 @@ pip install .
98
98
 
99
99
  ## 🎮 Play
100
100
 
101
- Run it inside any git repository no arguments needed:
101
+ Two commands works on any machine, even with no repo around:
102
102
 
103
103
  ```bash
104
+ pip install gitquest
104
105
  gitquest
105
106
  ```
106
107
 
107
- `python -m gitquest` works too.
108
+ If you run `gitquest` **inside a git repository**, it builds the dungeon from
109
+ *your* commit history. If you run it anywhere else, it automatically launches a
110
+ built-in **demo dungeon** so you can play right away.
111
+
112
+ `python -m gitquest` works too. Want your own repo? `cd` into it first, or pass
113
+ `--path`:
114
+
115
+ ```bash
116
+ cd path/to/your/repo && gitquest
117
+ gitquest --path ../some-other-repo
118
+ gitquest --demo # force the built-in demo dungeon
119
+ ```
120
+
121
+ ### Combat controls
122
+
123
+ During a fight, type one key + Enter: `a` attack · `d` defend · `i` item ·
124
+ `r` run.
108
125
 
109
126
  ### Flags
110
127
 
@@ -112,6 +129,7 @@ gitquest
112
129
  | ---------------- | --------------------------------------------------------- |
113
130
  | `--path <repo>` | Play on another repository (default: current directory). |
114
131
  | `--seed <n>` | Force a specific dungeon seed (default: derived from repo).|
132
+ | `--demo` | Play the built-in demo dungeon (no git repo required). |
115
133
  | `--fast` | Skip animations and "press Enter" pauses. |
116
134
  | `--stats-only` | Skip gameplay and print just the flex card. |
117
135
  | `--max-commits` | Cap/sample commits for huge repos (default: 300). |
@@ -5,5 +5,5 @@ commits, fight keyword-spawned monsters, gain language XP, and finish with a
5
5
  shareable career flex card.
6
6
  """
7
7
 
8
- __version__ = "0.1.0"
8
+ __version__ = "0.2.0"
9
9
  __all__ = ["__version__"]
@@ -68,6 +68,11 @@ def build_parser() -> argparse.ArgumentParser:
68
68
  metavar="N",
69
69
  help="cap/sample commits for very large repos (default: 300)",
70
70
  )
71
+ parser.add_argument(
72
+ "--demo",
73
+ action="store_true",
74
+ help="play a built-in demo dungeon (no git repo required)",
75
+ )
71
76
  parser.add_argument(
72
77
  "--version",
73
78
  action="version",
@@ -103,29 +108,51 @@ def main(argv: list[str] | None = None) -> int:
103
108
 
104
109
  console = Console()
105
110
 
106
- # --- Parse the repository --------------------------------------------- #
107
- try:
108
- repo = parse_repo(args.path, max_commits=args.max_commits)
109
- except NotAGitRepoError:
110
- console.print(
111
- f"[bold red]✗ {os.path.abspath(args.path)} is not a git repository.[/bold red]\n"
112
- "[grey62]Run gitquest from inside a repo, or pass --path <repo>.[/grey62]"
113
- )
114
- return 2
115
- except EmptyHistoryError:
111
+ # --- Parse the repository (or fall back to the built-in demo) --------- #
112
+ demo_notice = False
113
+ if args.demo:
114
+ from .demo import build_demo_repo
115
+
116
+ repo = build_demo_repo()
117
+ demo_notice = True
118
+ else:
119
+ try:
120
+ repo = parse_repo(args.path, max_commits=args.max_commits)
121
+ except (NotAGitRepoError, EmptyHistoryError):
122
+ # Zero-friction: if there's no repo to read, play the demo dungeon
123
+ # so `pip install gitquest && gitquest` always works anywhere.
124
+ from .demo import build_demo_repo
125
+
126
+ repo = build_demo_repo()
127
+ demo_notice = True
128
+ except Exception as exc: # pragma: no cover - defensive
129
+ console.print(f"[bold red]✗ Failed to read repository:[/bold red] {exc}")
130
+ return 1
131
+
132
+ if demo_notice and not args.stats_only:
116
133
  console.print(
117
- "[bold red] This repository has no commits yet.[/bold red]\n"
118
- "[grey62]Make a commit and try again — every legend starts somewhere.[/grey62]"
134
+ "[yellow] No git repository here launching the built-in demo "
135
+ "dungeon.[/yellow]\n"
136
+ "[grey62] To play your own commits, run gitquest inside a git repo "
137
+ "(or pass --path <repo>).[/grey62]\n"
119
138
  )
120
- return 2
121
- except Exception as exc: # pragma: no cover - defensive
122
- console.print(f"[bold red]✗ Failed to read repository:[/bold red] {exc}")
123
- return 1
124
139
 
125
140
  # --- Generate the dungeon --------------------------------------------- #
126
141
  dungeon = generate_dungeon(repo, user_seed=args.seed)
127
142
  player = Player(name=repo.head.author_name or "Hero")
128
143
 
144
+ # Every hero starts with a healing draught so the first room is survivable.
145
+ from .entities import Item, ItemKind
146
+
147
+ player.inventory.append(
148
+ Item(
149
+ name="Starter Health Draught",
150
+ kind=ItemKind.POTION,
151
+ value=15,
152
+ description="Restores 15 HP.",
153
+ )
154
+ )
155
+
129
156
  # --- Stats-only: silent auto run, then just the flex card ------------- #
130
157
  if args.stats_only:
131
158
  engine = GameEngine(dungeon, player, interactive=False)
@@ -0,0 +1,79 @@
1
+ """A built-in demo repository so gitquest is playable anywhere.
2
+
3
+ When ``gitquest`` is run outside a git repository (e.g. right after
4
+ ``pip install gitquest`` on a fresh machine), the CLI falls back to this
5
+ fictional commit history so there's always a dungeon to play. It is fully
6
+ deterministic, so the demo dungeon is identical on every device.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from datetime import datetime, timedelta, timezone
12
+
13
+ from .git_parser import CommitInfo, RepoData
14
+
15
+ _BASE = datetime(2024, 1, 1, 9, 0, 0, tzinfo=timezone.utc)
16
+
17
+ # (author, summary, insertions, deletions, files, is_merge)
18
+ _SCRIPT: list[tuple[str, str, int, int, list[str], bool]] = [
19
+ ("Ada Lovelace", "feat: scaffold the Dragon-Slayer API", 180, 0,
20
+ ["app.py", "server.py", "requirements.txt"], False),
21
+ ("Ada Lovelace", "feat: add /quests endpoint", 96, 4,
22
+ ["app.py", "routes/quests.py"], False),
23
+ ("Grace Hopper", "fix: null pointer when quest list is empty", 12, 8,
24
+ ["routes/quests.py"], False),
25
+ ("Grace Hopper", "test: add coverage for quest routes", 140, 2,
26
+ ["tests/test_quests.py"], False),
27
+ ("Linus T.", "refactor: extract quest service layer", 210, 160,
28
+ ["routes/quests.py", "services/quest_service.py"], False),
29
+ ("Margaret H.", "feat: shiny new front-end dashboard", 320, 10,
30
+ ["web/index.html", "web/app.js", "web/styles.css"], False),
31
+ ("Margaret H.", "fix: race condition in websocket handler", 24, 18,
32
+ ["web/app.js"], False),
33
+ ("Linus T.", "docs: write the README and API guide", 90, 1,
34
+ ["README.md", "docs/api.md"], False),
35
+ ("Grace Hopper", "Merge branch 'feature/realtime'", 6, 0,
36
+ ["web/app.js"], True),
37
+ ("Dennis R.", "feat: add authentication middleware", 150, 12,
38
+ ["middleware/auth.py", "app.py"], False),
39
+ ("Dennis R.", "fix: hotfix expired token crash", 30, 22,
40
+ ["middleware/auth.py"], False),
41
+ ("Ada Lovelace", "refactor: tidy config and add type hints", 88, 96,
42
+ ["config.py", "app.py"], False),
43
+ ("Barbara L.", "build: bump deps and dockerize", 64, 9,
44
+ ["Dockerfile", "requirements.txt", "docker-compose.yml"], False),
45
+ ("Grace Hopper", "Merge pull request #42 from feature/auth", 8, 0,
46
+ ["app.py"], True),
47
+ ]
48
+
49
+
50
+ def build_demo_repo() -> RepoData:
51
+ """Return a deterministic, fictional :class:`RepoData` for demo play."""
52
+ commits: list[CommitInfo] = []
53
+ for i, (author, summary, ins, dels, files, is_merge) in enumerate(_SCRIPT):
54
+ sha = f"{i:02d}" + "d" * 38 # stable, fake-but-valid-looking 40-char sha
55
+ email = author.lower().replace(" ", ".").replace(".", "") + "@gitquest.dev"
56
+ commits.append(
57
+ CommitInfo(
58
+ sha=sha,
59
+ short_sha=sha[:7],
60
+ author_name=author,
61
+ author_email=email,
62
+ timestamp=_BASE + timedelta(days=i, hours=i % 5),
63
+ summary=summary,
64
+ message=summary,
65
+ insertions=ins,
66
+ deletions=dels,
67
+ files=list(files),
68
+ is_merge=is_merge,
69
+ parent_count=2 if is_merge else 1,
70
+ )
71
+ )
72
+
73
+ return RepoData(
74
+ name="dragon-slayer-api (demo)",
75
+ path="<built-in demo>",
76
+ commits=commits,
77
+ sampled=False,
78
+ total_commits=len(commits),
79
+ )
@@ -10,6 +10,7 @@ from __future__ import annotations
10
10
 
11
11
  import hashlib
12
12
  import json
13
+ import math
13
14
  import random
14
15
  from functools import lru_cache
15
16
  from importlib import resources
@@ -110,19 +111,23 @@ def _make_monster(
110
111
  name = rng.choice(spec["names"])
111
112
  art = spec.get("art", "(o_o)")
112
113
 
114
+ # Compress churn with sqrt so a huge initial commit isn't 10x a small one,
115
+ # and apply an early-game ramp so the first rooms are gentle (the player
116
+ # has no levels or loot yet). Difficulty reaches full strength by ~room 10.
113
117
  capped = min(churn, 600)
114
- depth_bonus = depth * 0.9
115
- jitter = rng.uniform(0.85, 1.2)
118
+ cf = math.sqrt(capped)
119
+ ramp = min(1.0, 0.45 + depth * 0.06)
120
+ jitter = rng.uniform(0.9, 1.15)
116
121
 
117
- hp = int((8 + capped / 10 + depth_bonus) * jitter) + 1
118
- attack = int((3 + capped / 45 + depth * 0.30) * jitter) + 1
119
- defense = int(capped / 150 + depth * 0.12)
120
- xp_reward = int(9 + capped / 9 + depth * 1.6)
121
- gold_reward = int(rng.randint(2, 8) + capped / 25 + depth * 0.5)
122
+ hp = int((7 + cf * 0.9 + depth * 0.7) * ramp * jitter) + 1
123
+ attack = int((2 + cf * 0.22 + depth * 0.25) * ramp * jitter) + 1
124
+ defense = int((cf * 0.06 + depth * 0.10) * ramp)
125
+ xp_reward = int(9 + cf * 1.1 + depth * 1.6)
126
+ gold_reward = int(rng.randint(2, 8) + cf * 0.4 + depth * 0.5)
122
127
 
123
128
  if is_boss:
124
- hp = int(hp * 2.3) + 12
125
- attack = int(attack * 1.4) + 2
129
+ hp = int(hp * 2.2) + 10
130
+ attack = int(attack * 1.35) + 2
126
131
  defense = int(defense * 1.3) + 1
127
132
  xp_reward = int(xp_reward * 3)
128
133
  gold_reward = int(gold_reward * 3) + 10
@@ -214,7 +219,7 @@ def _build_room(
214
219
  monsters.append(_make_monster(rng, "Boss", churn, index, depth_total, True))
215
220
  else:
216
221
  monsters.append(_make_monster(rng, archetype, churn, index, depth_total, False))
217
- if churn >= 150 and rng.random() < 0.5:
222
+ if churn >= 200 and index > 3 and rng.random() < 0.45:
218
223
  monsters.append(
219
224
  _make_monster(rng, archetype, churn // 2, index, depth_total, False)
220
225
  )
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: gitquest
3
- Version: 0.1.0
3
+ Version: 0.2.0
4
4
  Summary: Turn your git commit history into a playable ASCII dungeon roguelike.
5
5
  Author-email: jithin-jz <jithinjzx@gmail.com>
6
6
  License-Expression: MIT
@@ -126,13 +126,30 @@ pip install .
126
126
 
127
127
  ## 🎮 Play
128
128
 
129
- Run it inside any git repository no arguments needed:
129
+ Two commands works on any machine, even with no repo around:
130
130
 
131
131
  ```bash
132
+ pip install gitquest
132
133
  gitquest
133
134
  ```
134
135
 
135
- `python -m gitquest` works too.
136
+ If you run `gitquest` **inside a git repository**, it builds the dungeon from
137
+ *your* commit history. If you run it anywhere else, it automatically launches a
138
+ built-in **demo dungeon** so you can play right away.
139
+
140
+ `python -m gitquest` works too. Want your own repo? `cd` into it first, or pass
141
+ `--path`:
142
+
143
+ ```bash
144
+ cd path/to/your/repo && gitquest
145
+ gitquest --path ../some-other-repo
146
+ gitquest --demo # force the built-in demo dungeon
147
+ ```
148
+
149
+ ### Combat controls
150
+
151
+ During a fight, type one key + Enter: `a` attack · `d` defend · `i` item ·
152
+ `r` run.
136
153
 
137
154
  ### Flags
138
155
 
@@ -140,6 +157,7 @@ gitquest
140
157
  | ---------------- | --------------------------------------------------------- |
141
158
  | `--path <repo>` | Play on another repository (default: current directory). |
142
159
  | `--seed <n>` | Force a specific dungeon seed (default: derived from repo).|
160
+ | `--demo` | Play the built-in demo dungeon (no git repo required). |
143
161
  | `--fast` | Skip animations and "press Enter" pauses. |
144
162
  | `--stats-only` | Skip gameplay and print just the flex card. |
145
163
  | `--max-commits` | Cap/sample commits for huge repos (default: 300). |
@@ -5,6 +5,7 @@ gitquest/__init__.py
5
5
  gitquest/__main__.py
6
6
  gitquest/cli.py
7
7
  gitquest/combat.py
8
+ gitquest/demo.py
8
9
  gitquest/engine.py
9
10
  gitquest/entities.py
10
11
  gitquest/flexcard.py
@@ -21,5 +22,6 @@ gitquest/data/items.json
21
22
  gitquest/data/monsters.json
22
23
  gitquest/data/ranks.json
23
24
  tests/test_combat.py
25
+ tests/test_demo.py
24
26
  tests/test_generator.py
25
27
  tests/test_git_parser.py
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "gitquest"
7
- version = "0.1.0"
7
+ version = "0.2.0"
8
8
  description = "Turn your git commit history into a playable ASCII dungeon roguelike."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -0,0 +1,51 @@
1
+ """Tests for the built-in demo repo and a full auto playthrough."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import random
6
+
7
+ from gitquest.demo import build_demo_repo
8
+ from gitquest.engine import GameEngine
9
+ from gitquest.entities import Item, ItemKind, Player
10
+ from gitquest.generator import generate_dungeon
11
+
12
+
13
+ def test_demo_repo_is_valid():
14
+ repo = build_demo_repo()
15
+ assert len(repo.commits) >= 10
16
+ assert repo.merge_count >= 1
17
+ assert repo.head.is_merge # last demo commit is a merge -> boss finale
18
+ assert "Python" in repo.language_totals
19
+
20
+
21
+ def test_demo_is_deterministic():
22
+ d1 = generate_dungeon(build_demo_repo())
23
+ d2 = generate_dungeon(build_demo_repo())
24
+ assert d1 == d2
25
+
26
+
27
+ def test_auto_playthrough_completes_and_is_winnable():
28
+ """The demo should be beatable by the auto strategy (with a starter potion)."""
29
+ dungeon = generate_dungeon(build_demo_repo())
30
+ player = Player(name="Hero")
31
+ player.inventory.append(
32
+ Item("Starter Health Draught", ItemKind.POTION, 15, description="Restores 15 HP.")
33
+ )
34
+ engine = GameEngine(
35
+ dungeon, player, rng=random.Random(dungeon.seed ^ 0x5EED), interactive=False
36
+ )
37
+ engine.run()
38
+
39
+ assert player.is_alive, "auto playthrough should survive the demo dungeon"
40
+ assert player.rooms_cleared == dungeon.size
41
+ assert player.bosses_slain >= 1
42
+ assert player.level > 1
43
+
44
+
45
+ def test_first_room_is_survivable():
46
+ """Early rooms must be gentle: the first monster should be weaker than the hero."""
47
+ dungeon = generate_dungeon(build_demo_repo())
48
+ first_monster = dungeon.rooms[0].monsters[0]
49
+ fresh = Player(name="Hero")
50
+ # The opening monster shouldn't out-stat a level-1 hero on both axes.
51
+ assert not (first_monster.hp > fresh.max_hp and first_monster.attack > fresh.attack)
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes