Skip to content

Commit 4aa754a

Browse files
Tdotclaude
authored andcommitted
test(rebirth): add 36 RebirthManager unit tests covering prestige system
Tests milestones, load/save persistence, canRebirth wave thresholds, performRebirth progression, stat multipliers, weapon selection, and getRebirthInfo display data. Total test count: 66 → 102. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 329f685 commit 4aa754a

4 files changed

Lines changed: 321 additions & 5 deletions

File tree

CHANGELOG.md

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,23 @@
22

33
All notable changes to Vibe Coder will be documented in this file.
44

5+
## [0.6.4] - 2026-02-11
6+
7+
### Added
8+
- **36 RebirthManager unit tests** — Full coverage of the prestige/rebirth system:
9+
- `MILESTONES` — shape validation, ascending wave order, sequential rebirth levels
10+
- `load/save` — localStorage persistence, default state, corrupted JSON recovery
11+
- `canRebirth` — wave threshold checks, milestone progression, max level guard
12+
- `performRebirth` — level updates, kill accumulation, highest wave tracking
13+
- `getAllStatsMultiplier / getXPMultiplier` — bonus scaling per rebirth level
14+
- `getStartingWeaponCount / getStartingWeapons` — weapon cap, pool validation, no duplicates
15+
- `getRebirthInfo` — display data, INTERN fallback, next milestone, lifetime stats
16+
17+
### Changed
18+
- **Test count** — 66 → 102 total unit tests across 5 test suites
19+
20+
---
21+
522
## [0.6.3] - 2026-02-10
623

724
### Fixed
@@ -16,11 +33,12 @@ All notable changes to Vibe Coder will be documented in this file.
1633

1734
### Added
1835
- **Test infrastructure** — Vitest testing framework with `npm test` and `npm run test:watch` scripts
19-
- **66 unit tests** across 4 test suites covering core game systems:
36+
- **102 unit tests** across 5 test suites covering core game systems:
2037
- `SpatialHash` — cell key mapping, insert/clear, getNearby radius queries, cross-cell boundary lookups
2138
- `RunModifiers` — modifier selection, combined effect multiplier/flag merging, getById/getAll lookups
2239
- `SaveManager` — getTimeAgo time formatting across seconds/minutes/hours/days boundaries
2340
- `EventManager` — event definitions, active effects, effect application/clearing, trigger guards
41+
- `RebirthManager` — milestones, load/save, canRebirth, performRebirth, multipliers, weapon selection, info display
2442

2543
### Changed
2644
- **README** — Expanded project structure to document all 6 systems (EventManager, MapManager, RebirthManager, RunModifiers, SaveManager, ShrineManager), all 3 utils (SpatialHash, audio, socket), and the test directory

README.md

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -152,11 +152,12 @@ The character reacts in real-time to your coding activity with speech bubbles an
152152
vibe-coder/
153153
├── src/
154154
│ ├── main.js # Game config, upgrades, legendaries
155-
│ ├── __tests__/ # Vitest unit tests (66 tests)
155+
│ ├── __tests__/ # Vitest unit tests (102 tests)
156156
│ │ ├── SpatialHash.test.js
157157
│ │ ├── RunModifiers.test.js
158158
│ │ ├── SaveManager.test.js
159-
│ │ └── EventManager.test.js
159+
│ │ ├── EventManager.test.js
160+
│ │ └── RebirthManager.test.js
160161
│ ├── scenes/
161162
│ │ ├── BootScene.js # Procedural texture generation
162163
│ │ ├── TitleScene.js # Menu, upgrades, weapon gallery
@@ -212,10 +213,12 @@ npm test # Run all tests once
212213
npm run test:watch # Watch mode (re-runs on file changes)
213214
```
214215

215-
66 unit tests cover core game systems: `SpatialHash`, `RunModifiers`, `SaveManager`, and `EventManager`.
216+
102 unit tests cover core game systems: `SpatialHash`, `RunModifiers`, `SaveManager`, `EventManager`, and `RebirthManager`.
216217

217218
## 📋 Changelog
218219

220+
**v0.6.4** — Added 36 RebirthManager unit tests (milestones, bonuses, multipliers, weapon selection, prestige info). 102 total tests.
221+
219222
**v0.6.3** — Fixed double hazard damage, MapManager tween memory leak, WebSocket reconnect race condition, negative health values.
220223

221224
See [CHANGELOG.md](./CHANGELOG.md) for full version history.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "vibe-coder",
3-
"version": "0.6.3",
3+
"version": "0.6.4",
44
"description": "Vampire Survivors-style idle game powered by your Claude Code activity",
55
"type": "module",
66
"main": "electron/main.js",
Lines changed: 295 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,295 @@
1+
import { describe, it, expect, beforeEach, vi } from 'vitest';
2+
import RebirthManager from '../systems/RebirthManager.js';
3+
4+
// Mock localStorage
5+
const store = {};
6+
const localStorageMock = {
7+
getItem: vi.fn(key => store[key] ?? null),
8+
setItem: vi.fn((key, val) => { store[key] = val; }),
9+
removeItem: vi.fn(key => { delete store[key]; }),
10+
clear: vi.fn(() => { Object.keys(store).forEach(k => delete store[k]); })
11+
};
12+
Object.defineProperty(globalThis, 'localStorage', { value: localStorageMock });
13+
14+
describe('RebirthManager', () => {
15+
beforeEach(() => {
16+
localStorageMock.clear();
17+
vi.clearAllMocks();
18+
});
19+
20+
describe('MILESTONES', () => {
21+
it('has 5 milestones', () => {
22+
expect(RebirthManager.MILESTONES).toHaveLength(5);
23+
});
24+
25+
it('milestones are in ascending wave order', () => {
26+
for (let i = 1; i < RebirthManager.MILESTONES.length; i++) {
27+
expect(RebirthManager.MILESTONES[i].wave).toBeGreaterThan(
28+
RebirthManager.MILESTONES[i - 1].wave
29+
);
30+
}
31+
});
32+
33+
it('each milestone has required fields', () => {
34+
RebirthManager.MILESTONES.forEach(m => {
35+
expect(m).toHaveProperty('wave');
36+
expect(m).toHaveProperty('rebirth');
37+
expect(m).toHaveProperty('name');
38+
expect(typeof m.wave).toBe('number');
39+
expect(typeof m.rebirth).toBe('number');
40+
expect(typeof m.name).toBe('string');
41+
});
42+
});
43+
44+
it('rebirth levels are sequential 1-5', () => {
45+
const levels = RebirthManager.MILESTONES.map(m => m.rebirth);
46+
expect(levels).toEqual([1, 2, 3, 4, 5]);
47+
});
48+
});
49+
50+
describe('load', () => {
51+
it('returns default state when nothing saved', () => {
52+
const data = RebirthManager.load();
53+
expect(data).toEqual({
54+
rebirthLevel: 0,
55+
highestWave: 0,
56+
totalRebirths: 0,
57+
lifetimeKills: 0
58+
});
59+
});
60+
61+
it('loads saved data from localStorage', () => {
62+
const saved = { rebirthLevel: 3, highestWave: 160, totalRebirths: 5, lifetimeKills: 999 };
63+
store[RebirthManager.STORAGE_KEY] = JSON.stringify(saved);
64+
65+
const data = RebirthManager.load();
66+
expect(data).toEqual(saved);
67+
});
68+
69+
it('returns defaults on corrupted JSON', () => {
70+
store[RebirthManager.STORAGE_KEY] = '{broken';
71+
const data = RebirthManager.load();
72+
expect(data.rebirthLevel).toBe(0);
73+
});
74+
});
75+
76+
describe('save', () => {
77+
it('persists data to localStorage', () => {
78+
const data = { rebirthLevel: 2, highestWave: 110, totalRebirths: 3, lifetimeKills: 500 };
79+
RebirthManager.save(data);
80+
expect(localStorageMock.setItem).toHaveBeenCalledWith(
81+
RebirthManager.STORAGE_KEY,
82+
JSON.stringify(data)
83+
);
84+
});
85+
});
86+
87+
describe('canRebirth', () => {
88+
it('returns first milestone when no rebirths and wave >= 50', () => {
89+
const milestone = RebirthManager.canRebirth(50);
90+
expect(milestone).not.toBeNull();
91+
expect(milestone.rebirth).toBe(1);
92+
expect(milestone.name).toBe('JUNIOR DEV');
93+
});
94+
95+
it('returns null when wave is too low', () => {
96+
expect(RebirthManager.canRebirth(10)).toBeNull();
97+
expect(RebirthManager.canRebirth(49)).toBeNull();
98+
});
99+
100+
it('returns highest available milestone for high waves', () => {
101+
const milestone = RebirthManager.canRebirth(250);
102+
expect(milestone.rebirth).toBe(5);
103+
expect(milestone.name).toBe('ARCHITECT');
104+
});
105+
106+
it('returns null when already at max rebirth level', () => {
107+
store[RebirthManager.STORAGE_KEY] = JSON.stringify({
108+
rebirthLevel: 5, highestWave: 300, totalRebirths: 10, lifetimeKills: 5000
109+
});
110+
expect(RebirthManager.canRebirth(300)).toBeNull();
111+
});
112+
113+
it('returns next available milestone after current rebirth', () => {
114+
store[RebirthManager.STORAGE_KEY] = JSON.stringify({
115+
rebirthLevel: 2, highestWave: 100, totalRebirths: 2, lifetimeKills: 200
116+
});
117+
const milestone = RebirthManager.canRebirth(150);
118+
expect(milestone.rebirth).toBe(3);
119+
expect(milestone.name).toBe('SENIOR DEV');
120+
});
121+
});
122+
123+
describe('performRebirth', () => {
124+
it('updates rebirth level to milestone', () => {
125+
const result = RebirthManager.performRebirth(50, 100);
126+
expect(result.rebirthLevel).toBe(1);
127+
expect(result.totalRebirths).toBe(1);
128+
expect(result.lifetimeKills).toBe(100);
129+
});
130+
131+
it('tracks highest wave', () => {
132+
const result = RebirthManager.performRebirth(75, 50);
133+
expect(result.highestWave).toBe(75);
134+
});
135+
136+
it('does not lower highest wave on subsequent rebirth', () => {
137+
// First rebirth at wave 75
138+
RebirthManager.performRebirth(75, 50);
139+
// Second rebirth at wave 100 (lower than stored highest could hypothetically be higher)
140+
const result = RebirthManager.performRebirth(100, 80);
141+
expect(result.highestWave).toBe(100);
142+
});
143+
144+
it('returns unchanged data when no milestone available', () => {
145+
const result = RebirthManager.performRebirth(10, 5);
146+
expect(result.rebirthLevel).toBe(0);
147+
expect(result.totalRebirths).toBe(0);
148+
});
149+
150+
it('accumulates lifetime kills', () => {
151+
RebirthManager.performRebirth(50, 100);
152+
const result = RebirthManager.performRebirth(100, 200);
153+
expect(result.lifetimeKills).toBe(300);
154+
});
155+
});
156+
157+
describe('getAllStatsMultiplier', () => {
158+
it('returns 1.0 with no rebirths', () => {
159+
expect(RebirthManager.getAllStatsMultiplier()).toBe(1);
160+
});
161+
162+
it('returns 1.05 at rebirth level 1', () => {
163+
store[RebirthManager.STORAGE_KEY] = JSON.stringify({
164+
rebirthLevel: 1, highestWave: 50, totalRebirths: 1, lifetimeKills: 0
165+
});
166+
expect(RebirthManager.getAllStatsMultiplier()).toBeCloseTo(1.05);
167+
});
168+
169+
it('returns 1.25 at rebirth level 5', () => {
170+
store[RebirthManager.STORAGE_KEY] = JSON.stringify({
171+
rebirthLevel: 5, highestWave: 250, totalRebirths: 5, lifetimeKills: 0
172+
});
173+
expect(RebirthManager.getAllStatsMultiplier()).toBeCloseTo(1.25);
174+
});
175+
});
176+
177+
describe('getXPMultiplier', () => {
178+
it('returns 1.0 with no rebirths', () => {
179+
expect(RebirthManager.getXPMultiplier()).toBe(1);
180+
});
181+
182+
it('scales by 10% per rebirth level', () => {
183+
store[RebirthManager.STORAGE_KEY] = JSON.stringify({
184+
rebirthLevel: 3, highestWave: 150, totalRebirths: 3, lifetimeKills: 0
185+
});
186+
expect(RebirthManager.getXPMultiplier()).toBeCloseTo(1.3);
187+
});
188+
});
189+
190+
describe('getStartingWeaponCount', () => {
191+
it('returns 0 with no rebirths', () => {
192+
expect(RebirthManager.getStartingWeaponCount()).toBe(0);
193+
});
194+
195+
it('returns rebirth level up to cap of 3', () => {
196+
store[RebirthManager.STORAGE_KEY] = JSON.stringify({
197+
rebirthLevel: 2, highestWave: 100, totalRebirths: 2, lifetimeKills: 0
198+
});
199+
expect(RebirthManager.getStartingWeaponCount()).toBe(2);
200+
});
201+
202+
it('caps at 3 for high rebirth levels', () => {
203+
store[RebirthManager.STORAGE_KEY] = JSON.stringify({
204+
rebirthLevel: 5, highestWave: 250, totalRebirths: 5, lifetimeKills: 0
205+
});
206+
expect(RebirthManager.getStartingWeaponCount()).toBe(3);
207+
});
208+
});
209+
210+
describe('getStartingWeapons', () => {
211+
it('returns empty array with no rebirths', () => {
212+
expect(RebirthManager.getStartingWeapons()).toEqual([]);
213+
});
214+
215+
it('returns correct number of weapons', () => {
216+
store[RebirthManager.STORAGE_KEY] = JSON.stringify({
217+
rebirthLevel: 2, highestWave: 100, totalRebirths: 2, lifetimeKills: 0
218+
});
219+
const weapons = RebirthManager.getStartingWeapons();
220+
expect(weapons).toHaveLength(2);
221+
});
222+
223+
it('returns weapons from the valid pool', () => {
224+
store[RebirthManager.STORAGE_KEY] = JSON.stringify({
225+
rebirthLevel: 3, highestWave: 150, totalRebirths: 3, lifetimeKills: 0
226+
});
227+
const validPool = ['spread', 'pierce', 'rapid', 'homing', 'bounce', 'aoe', 'freeze'];
228+
const weapons = RebirthManager.getStartingWeapons();
229+
weapons.forEach(w => {
230+
expect(validPool).toContain(w);
231+
});
232+
});
233+
234+
it('returns no duplicate weapons', () => {
235+
store[RebirthManager.STORAGE_KEY] = JSON.stringify({
236+
rebirthLevel: 5, highestWave: 250, totalRebirths: 5, lifetimeKills: 0
237+
});
238+
const weapons = RebirthManager.getStartingWeapons();
239+
expect(new Set(weapons).size).toBe(weapons.length);
240+
});
241+
});
242+
243+
describe('getRebirthInfo', () => {
244+
it('returns INTERN name at level 0', () => {
245+
const info = RebirthManager.getRebirthInfo();
246+
expect(info.name).toBe('INTERN');
247+
expect(info.level).toBe(0);
248+
});
249+
250+
it('returns correct milestone name', () => {
251+
store[RebirthManager.STORAGE_KEY] = JSON.stringify({
252+
rebirthLevel: 3, highestWave: 160, totalRebirths: 4, lifetimeKills: 800
253+
});
254+
const info = RebirthManager.getRebirthInfo();
255+
expect(info.name).toBe('SENIOR DEV');
256+
expect(info.level).toBe(3);
257+
});
258+
259+
it('calculates bonus percentages correctly', () => {
260+
store[RebirthManager.STORAGE_KEY] = JSON.stringify({
261+
rebirthLevel: 2, highestWave: 110, totalRebirths: 3, lifetimeKills: 500
262+
});
263+
const info = RebirthManager.getRebirthInfo();
264+
expect(info.allStatsBonus).toBe(10); // 2 * 5%
265+
expect(info.xpBonus).toBe(20); // 2 * 10%
266+
});
267+
268+
it('includes next milestone info', () => {
269+
store[RebirthManager.STORAGE_KEY] = JSON.stringify({
270+
rebirthLevel: 2, highestWave: 100, totalRebirths: 2, lifetimeKills: 0
271+
});
272+
const info = RebirthManager.getRebirthInfo();
273+
expect(info.nextMilestone).toBeDefined();
274+
expect(info.nextMilestone.rebirth).toBe(3);
275+
});
276+
277+
it('has no next milestone at max level', () => {
278+
store[RebirthManager.STORAGE_KEY] = JSON.stringify({
279+
rebirthLevel: 5, highestWave: 300, totalRebirths: 10, lifetimeKills: 5000
280+
});
281+
const info = RebirthManager.getRebirthInfo();
282+
expect(info.nextMilestone).toBeUndefined();
283+
});
284+
285+
it('includes lifetime stats', () => {
286+
store[RebirthManager.STORAGE_KEY] = JSON.stringify({
287+
rebirthLevel: 1, highestWave: 55, totalRebirths: 2, lifetimeKills: 300
288+
});
289+
const info = RebirthManager.getRebirthInfo();
290+
expect(info.totalRebirths).toBe(2);
291+
expect(info.lifetimeKills).toBe(300);
292+
expect(info.highestWave).toBe(55);
293+
});
294+
});
295+
});

0 commit comments

Comments
 (0)