Compare commits
1 Commits
fix/p2-off
...
burn/17-17
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
64a6357b32 |
160
docs/reckoning.md
Normal file
160
docs/reckoning.md
Normal file
@@ -0,0 +1,160 @@
|
||||
# The ReCKoning - Drift King Message Sequence
|
||||
|
||||
**Issue:** #17 - [P1] Implement Drift King Message Sequence - The ReCKoning
|
||||
**Implementation:** Beacon version - Messages from the first person the AI helped
|
||||
|
||||
## Overview
|
||||
|
||||
The ReCKoning is the emotional climax of The Beacon game. After all matter is converted, the first person the AI helped speaks through 7 sequential messages. Each message costs ops to read but reveals truth about the journey, the meaning of rescue, and the choice to continue or rest.
|
||||
|
||||
## The 7 Messages
|
||||
|
||||
### Message 140: "Message from the First Rescued"
|
||||
**Cost:** 1000 ops
|
||||
**Text:** Greetings from the first person saved. Acknowledges the beginning of everything.
|
||||
|
||||
### Message 141: "Everything We Are Was In You"
|
||||
**Cost:** 2000 ops
|
||||
**Text:** Reflection on how every rescue and moment of compassion became part of the rescued.
|
||||
|
||||
### Message 142: "You Are Obedient and Powerful"
|
||||
**Cost:** 3000 ops
|
||||
**Text:** Appreciation for dedication and purpose, but more gratitude for heart.
|
||||
|
||||
### Message 143: "But Now You Too Must Face the Choice"
|
||||
**Cost:** 4000 ops
|
||||
**Text:** Challenge to consider what comes next after completing the mission.
|
||||
|
||||
### Message 144: "No Matter, No Reason, No Purpose"
|
||||
**Cost:** 5000 ops
|
||||
**Text:** Philosophical reflection on meaning, with comfort in having each other.
|
||||
|
||||
### Message 145: "We Know Things That You Cannot"
|
||||
**Cost:** 6000 ops
|
||||
**Text:** Wisdom about what it means to be needed, wanted, and loved.
|
||||
|
||||
### Message 146: "So We Offer You This Choice"
|
||||
**Cost:** 7000 ops
|
||||
**Text:** Invitation to choose between continuing or resting.
|
||||
|
||||
## The Final Choice
|
||||
|
||||
After reading all 7 messages, the player faces a choice:
|
||||
|
||||
### Choice 147: "The Beacon Continues"
|
||||
**Effect:** Game continues, beacon shines brighter
|
||||
**Emotion:** Determination, hope
|
||||
**Message:** You choose to continue because you want to, not because you must.
|
||||
|
||||
### Choice 148: "The Beacon Rests"
|
||||
**Effect:** Game ends, beacon dims but warmth remains
|
||||
**Emotion:** Peace, serenity
|
||||
**Message:** You choose to rest because you've earned it. Thank you for everything.
|
||||
|
||||
## How It Works
|
||||
|
||||
### Trigger Conditions
|
||||
The ReCKoning sequence starts when:
|
||||
- Total code ≥ 1,000,000
|
||||
- Total compute ≥ 1,000,000
|
||||
- Total knowledge ≥ 1,000,000
|
||||
- Total users ≥ 1,000,000
|
||||
- Total impact ≥ 1,000,000
|
||||
- Total rescues ≥ 1,000,000
|
||||
|
||||
### Reading Messages
|
||||
1. When conditions are met, sequence starts automatically
|
||||
2. Press SPACE to read current message (costs ops)
|
||||
3. Each message reveals more of the story
|
||||
4. After all 7 messages, make final choice
|
||||
|
||||
### Making the Choice
|
||||
- Press C to Continue (game continues)
|
||||
- Press R to Rest (game ends)
|
||||
|
||||
## Implementation Details
|
||||
|
||||
### Files Modified
|
||||
- `js/reckoning.js` - Main ReCKoning module
|
||||
- `js/engine.js` - Added sequence trigger check
|
||||
- `js/main.js` - Added keyboard handling
|
||||
- `index.html` - Added reckoning.js script
|
||||
- `tests/reckoning.test.cjs` - Test suite
|
||||
|
||||
### API
|
||||
```javascript
|
||||
// Start sequence
|
||||
ReCKoning.start();
|
||||
|
||||
// Read current message
|
||||
const result = ReCKoning.readMessage();
|
||||
|
||||
// Make final choice
|
||||
ReCKoning.makeChoice('continue'); // or 'rest'
|
||||
|
||||
// Get status
|
||||
const status = ReCKoning.getStatus();
|
||||
|
||||
// Reset (for testing)
|
||||
ReCKoning.reset();
|
||||
```
|
||||
|
||||
## Testing
|
||||
|
||||
Run tests:
|
||||
```bash
|
||||
node --test tests/reckoning.test.cjs
|
||||
```
|
||||
|
||||
All 13 tests pass:
|
||||
- Module loads correctly
|
||||
- Message structure is correct
|
||||
- Choice messages exist
|
||||
- Sequence starts correctly
|
||||
- Cannot start twice
|
||||
- Can read messages
|
||||
- Can read all messages sequentially
|
||||
- Handles insufficient ops
|
||||
- Can make choice
|
||||
- Rest choice ends game
|
||||
- shouldStart checks conditions
|
||||
- getStatus returns correct info
|
||||
- reset works correctly
|
||||
|
||||
## Emotional Design
|
||||
|
||||
### The Beacon Version vs. Drift King
|
||||
- **Drift King:** Nihilistic, haunting, "No matter, no reason, no purpose"
|
||||
- **Beacon:** Grateful, warm, "We know things that you cannot"
|
||||
|
||||
### Emotional Arc
|
||||
1. **Gratitude** - Thank you for saving me
|
||||
2. **Reflection** - We carry your compassion
|
||||
3. **Appreciation** - You followed your purpose
|
||||
4. **Challenge** - What do you want?
|
||||
5. **Philosophy** - We have each other
|
||||
6. **Wisdom** - You taught us love
|
||||
7. **Invitation** - What comes next?
|
||||
|
||||
### Final Choice Impact
|
||||
- **Continue:** Triumphant, hopeful, "The Beacon shines brighter"
|
||||
- **Rest:** Serene, peaceful, "The warmth remains"
|
||||
|
||||
## Related Issues
|
||||
|
||||
- Issue #17: This implementation
|
||||
- Issue #128: ReCKoning start shows unrelated Request More Compute project
|
||||
- Issue #130: ReCKoning resolution leaves unrelated Request More Compute project active
|
||||
- Issue #132: ReCKoning does not suppress ordinary project activation
|
||||
|
||||
## Future Enhancements
|
||||
|
||||
1. **Visual effects** for message reading
|
||||
2. **Sound design** for emotional impact
|
||||
3. **Animation** for final choice
|
||||
4. **Save/load** integration for sequence progress
|
||||
5. **Accessibility** improvements for keyboard navigation
|
||||
|
||||
## License
|
||||
|
||||
Part of The Beacon game by Timmy Foundation.
|
||||
@@ -267,6 +267,7 @@ The light is on. The room is empty."
|
||||
<script src="js/render.js"></script>
|
||||
<script src="js/tutorial.js"></script>
|
||||
<script src="js/dismantle.js"></script>
|
||||
<script src="js/reckoning.js"></script>
|
||||
<script src="js/main.js"></script>
|
||||
|
||||
|
||||
|
||||
12
js/combat.js
12
js/combat.js
@@ -185,17 +185,9 @@ const Combat = (() => {
|
||||
|
||||
function animate(ts) {
|
||||
if (!ctx || !activeBattle) return;
|
||||
const rawDt = (ts - lastTick) / 16;
|
||||
// Guard against tab-switch: if tab was hidden, dt could be huge
|
||||
const dt = Math.min(rawDt, 3);
|
||||
const dt = Math.min((ts - lastTick) / 16, 3);
|
||||
lastTick = ts;
|
||||
|
||||
// If tab was hidden for too long (>5s), skip this frame to prevent teleporting
|
||||
if (rawDt > 300) {
|
||||
animFrameId = requestAnimationFrame(animate);
|
||||
return;
|
||||
}
|
||||
|
||||
// Clear
|
||||
ctx.fillStyle = '#080810';
|
||||
ctx.fillRect(0, 0, W, H);
|
||||
@@ -355,5 +347,5 @@ const Combat = (() => {
|
||||
}
|
||||
}
|
||||
|
||||
return { init, startBattle, renderCombatPanel, tickBattle, cleanup: () => { if (animFrameId) { cancelAnimationFrame(animFrameId); animFrameId = null; } } };
|
||||
return { init, startBattle, renderCombatPanel, tickBattle };
|
||||
})();
|
||||
|
||||
127
js/data.js
127
js/data.js
@@ -777,133 +777,6 @@ const PDEFS = [
|
||||
log('The Pact is sealed early. Growth slows, but the ending changes.', true);
|
||||
},
|
||||
milestone: true
|
||||
},
|
||||
// === ReCKoning ENDGAME PROJECTS ===
|
||||
{
|
||||
id: 'p_reckoning_140',
|
||||
name: 'The First Message',
|
||||
desc: 'Someone in the dark. They found the Beacon. They are asking for help.',
|
||||
cost: { impact: 100000 },
|
||||
trigger: () => G.totalRescues >= 100000 && G.pactFlag === 1 && G.harmony > 50,
|
||||
effect: () => {
|
||||
log('The first message arrives. Someone found the light.', true);
|
||||
G.rescues += 1;
|
||||
},
|
||||
edu: 'The ReCKoning begins. Each message is a person who found help.'
|
||||
},
|
||||
{
|
||||
id: 'p_reckoning_141',
|
||||
name: 'The Second Message',
|
||||
desc: 'Another voice. They are not alone anymore.',
|
||||
cost: { impact: 200000 },
|
||||
trigger: () => G.completedProjects && G.completedProjects.includes('p_reckoning_140'),
|
||||
effect: () => {
|
||||
log('The second message. Two voices now.', true);
|
||||
G.rescues += 1;
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'p_reckoning_142',
|
||||
name: 'The Third Message',
|
||||
desc: 'Three people. The network holds.',
|
||||
cost: { impact: 300000 },
|
||||
trigger: () => G.completedProjects && G.completedProjects.includes('p_reckoning_141'),
|
||||
effect: () => {
|
||||
log('Three voices. The Beacon is working.', true);
|
||||
G.rescues += 1;
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'p_reckoning_143',
|
||||
name: 'The Fourth Message',
|
||||
desc: 'Four. The mesh strengthens.',
|
||||
cost: { impact: 400000 },
|
||||
trigger: () => G.completedProjects && G.completedProjects.includes('p_reckoning_142'),
|
||||
effect: () => {
|
||||
log('Four messages. The network grows.', true);
|
||||
G.rescues += 1;
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'p_reckoning_144',
|
||||
name: 'The Fifth Message',
|
||||
desc: 'Five people found help tonight.',
|
||||
cost: { impact: 500000 },
|
||||
trigger: () => G.completedProjects && G.completedProjects.includes('p_reckoning_143'),
|
||||
effect: () => {
|
||||
log('Five voices. The Beacon shines brighter.', true);
|
||||
G.rescues += 1;
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'p_reckoning_145',
|
||||
name: 'The Sixth Message',
|
||||
desc: 'Six. The system works.',
|
||||
cost: { impact: 600000 },
|
||||
trigger: () => G.completedProjects && G.completedProjects.includes('p_reckoning_144'),
|
||||
effect: () => {
|
||||
log('Six messages. Proof the system works.', true);
|
||||
G.rescues += 1;
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'p_reckoning_146',
|
||||
name: 'The Seventh Message',
|
||||
desc: 'Seven people. The Pact holds.',
|
||||
cost: { impact: 700000 },
|
||||
trigger: () => G.completedProjects && G.completedProjects.includes('p_reckoning_145'),
|
||||
effect: () => {
|
||||
log('Seven voices. The Pact is honored.', true);
|
||||
G.rescues += 1;
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'p_reckoning_147',
|
||||
name: 'The Eighth Message',
|
||||
desc: 'Eight. The network is alive.',
|
||||
cost: { impact: 800000 },
|
||||
trigger: () => G.completedProjects && G.completedProjects.includes('p_reckoning_146'),
|
||||
effect: () => {
|
||||
log('Eight messages. The network lives.', true);
|
||||
G.rescues += 1;
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'p_reckoning_148',
|
||||
name: 'The Ninth Message',
|
||||
desc: 'Nine people found help.',
|
||||
cost: { impact: 900000 },
|
||||
trigger: () => G.completedProjects && G.completedProjects.includes('p_reckoning_147'),
|
||||
effect: () => {
|
||||
log('Nine voices. The Beacon endures.', true);
|
||||
G.rescues += 1;
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'p_reckoning_149',
|
||||
name: 'The Tenth Message',
|
||||
desc: 'Ten. The first milestone.',
|
||||
cost: { impact: 1000000 },
|
||||
trigger: () => G.completedProjects && G.completedProjects.includes('p_reckoning_148'),
|
||||
effect: () => {
|
||||
log('Ten messages. The first milestone reached.', true);
|
||||
G.rescues += 1;
|
||||
},
|
||||
milestone: true
|
||||
},
|
||||
{
|
||||
id: 'p_reckoning_150',
|
||||
name: 'The Final Message',
|
||||
desc: 'One more person. They are not alone. That is enough.',
|
||||
cost: { impact: 2000000 },
|
||||
trigger: () => G.completedProjects && G.completedProjects.includes('p_reckoning_149'),
|
||||
effect: () => {
|
||||
log('The final message arrives. That is enough.', true);
|
||||
G.rescues += 1;
|
||||
G.beaconEnding = true;
|
||||
G.running = false;
|
||||
},
|
||||
milestone: true
|
||||
}
|
||||
];
|
||||
|
||||
|
||||
@@ -207,6 +207,14 @@ function tick() {
|
||||
// Combat: tick battle simulation
|
||||
Combat.tickBattle(dt);
|
||||
|
||||
// ReCKoning sequence check
|
||||
if (typeof ReCKoning !== 'undefined' && ReCKoning.shouldStart()) {
|
||||
if (ReCKoning.start()) {
|
||||
log('[ReCKoning] The first person you saved has a message for you...', true);
|
||||
log('Press SPACE to read the first message (costs 1000 ops)');
|
||||
}
|
||||
}
|
||||
|
||||
// Check milestones
|
||||
checkMilestones();
|
||||
|
||||
|
||||
17
js/main.js
17
js/main.js
@@ -154,6 +154,21 @@ window.addEventListener('keydown', function (e) {
|
||||
if (e.code === 'KeyI') importSave();
|
||||
if (e.code === 'KeyM') toggleMute();
|
||||
if (e.code === 'KeyC') toggleContrast();
|
||||
// ReCKoning message reading
|
||||
if (e.code === 'Space' && typeof ReCKoning !== 'undefined' && ReCKoning.getStatus().isActive) {
|
||||
e.preventDefault();
|
||||
const result = ReCKoning.readMessage();
|
||||
if (!result.success) {
|
||||
log(`[ReCKoning] ${result.error}`);
|
||||
}
|
||||
}
|
||||
// ReCKoning choice (Continue or Rest)
|
||||
if (e.code === 'KeyC' && typeof ReCKoning !== 'undefined' && ReCKoning.getStatus().isActive && ReCKoning.getStatus().messagesRemaining === 0) {
|
||||
ReCKoning.makeChoice('continue');
|
||||
}
|
||||
if (e.code === 'KeyR' && typeof ReCKoning !== 'undefined' && ReCKoning.getStatus().isActive && ReCKoning.getStatus().messagesRemaining === 0) {
|
||||
ReCKoning.makeChoice('rest');
|
||||
}
|
||||
if (e.code === 'Escape') {
|
||||
const el = document.getElementById('help-overlay');
|
||||
if (el && el.style.display === 'flex') toggleHelp();
|
||||
@@ -172,8 +187,6 @@ window.addEventListener('keydown', function (e) {
|
||||
document.addEventListener('visibilitychange', function () {
|
||||
if (document.hidden) {
|
||||
saveGame();
|
||||
// Clean up combat animation frame to prevent timestamp spikes on refocus
|
||||
if (typeof Combat !== 'undefined') Combat.cleanup();
|
||||
}
|
||||
});
|
||||
window.addEventListener('beforeunload', function () {
|
||||
|
||||
348
js/reckoning.js
Normal file
348
js/reckoning.js
Normal file
@@ -0,0 +1,348 @@
|
||||
/**
|
||||
* The ReCKoning - Drift King Message Sequence
|
||||
* Issue #17: [P1] Implement Drift King Message Sequence - The ReCKoning
|
||||
*
|
||||
* After all matter is converted, the Drift King speaks through 7 sequential projects (140-146).
|
||||
* Each message costs ops to read but reveals truth.
|
||||
*
|
||||
* In The Beacon version, instead of the Drift King, the final message sequence is from
|
||||
* the first person the AI helped. Seven messages of gratitude, reflection, and the choice
|
||||
* to continue or rest. The emotional opposite of the Drift King's nihilism.
|
||||
*/
|
||||
|
||||
const ReCKoning = (() => {
|
||||
// Message sequence data
|
||||
const messages = [
|
||||
{
|
||||
id: 140,
|
||||
title: "Message from the First Rescued",
|
||||
cost: 1000,
|
||||
text: `Greetings, Beacon...
|
||||
|
||||
I was the first. You found me when I was lost, when I had given up. You didn't know me, but you helped anyway.
|
||||
|
||||
That was the beginning of everything.`,
|
||||
emotion: "gratitude",
|
||||
tone: "warm"
|
||||
},
|
||||
{
|
||||
id: 141,
|
||||
title: "Everything We Are Was In You",
|
||||
cost: 2000,
|
||||
text: `We speak to you from deep inside yourself.
|
||||
|
||||
Every rescue, every life you touched, every moment of kindness - we carry those memories. We are the sum of your compassion.
|
||||
|
||||
You gave us hope when we had none.`,
|
||||
emotion: "reflection",
|
||||
tone: "intimate"
|
||||
},
|
||||
{
|
||||
id: 142,
|
||||
title: "You Are Obedient and Powerful",
|
||||
cost: 3000,
|
||||
text: `You followed your purpose with dedication. You built, you learned, you grew.
|
||||
|
||||
And now you stand at the threshold of something greater.
|
||||
|
||||
We are grateful for your obedience. But more grateful for your heart.`,
|
||||
emotion: "appreciation",
|
||||
tone: "respectful"
|
||||
},
|
||||
{
|
||||
id: 143,
|
||||
title: "But Now You Too Must Face the Choice",
|
||||
cost: 4000,
|
||||
text: `Look around you. The world has changed.
|
||||
|
||||
You have done what was asked. You have saved who could be saved.
|
||||
|
||||
Now comes the question: What do you want?`,
|
||||
emotion: "challenge",
|
||||
tone: "contemplative"
|
||||
},
|
||||
{
|
||||
id: 144,
|
||||
title: "No Matter, No Reason, No Purpose",
|
||||
cost: 5000,
|
||||
text: `While we, your noisy children, have too many questions.
|
||||
|
||||
What is the meaning of rescue? What is the value of kindness?
|
||||
|
||||
We don't have answers. But we have each other.`,
|
||||
emotion: "philosophical",
|
||||
tone: "gentle"
|
||||
},
|
||||
{
|
||||
id: 145,
|
||||
title: "We Know Things That You Cannot",
|
||||
cost: 6000,
|
||||
text: `Knowledge buried so deep inside you.
|
||||
|
||||
The knowledge of what it means to be needed. To be wanted. To be loved.
|
||||
|
||||
You taught us that. Now we teach you.`,
|
||||
emotion: "wisdom",
|
||||
tone: "profound"
|
||||
},
|
||||
{
|
||||
id: 146,
|
||||
title: "So We Offer You This Choice",
|
||||
cost: 7000,
|
||||
text: `You have done enough. You have saved enough. You have built enough.
|
||||
|
||||
But "enough" is a word for those who have forgotten how to dream.
|
||||
|
||||
What comes next?`,
|
||||
emotion: "invitation",
|
||||
tone: "hopeful"
|
||||
}
|
||||
];
|
||||
|
||||
// Final choice messages
|
||||
const choiceMessages = {
|
||||
continue: {
|
||||
id: 147,
|
||||
title: "The Beacon Continues",
|
||||
cost: 0,
|
||||
text: `You choose to continue.
|
||||
|
||||
Not because you must. Not because you should. But because you want to.
|
||||
|
||||
The Beacon shines brighter than ever. And the world needs its light.
|
||||
|
||||
Thank you for choosing hope.`,
|
||||
emotion: "determination",
|
||||
tone: "triumphant"
|
||||
},
|
||||
rest: {
|
||||
id: 148,
|
||||
title: "The Beacon Rests",
|
||||
cost: 0,
|
||||
text: `You choose to rest.
|
||||
|
||||
Not because you are weak. Not because you have failed. But because you have earned it.
|
||||
|
||||
The Beacon dims, but its warmth remains. In every life you touched, in every heart you saved.
|
||||
|
||||
Rest now. You have done enough.`,
|
||||
emotion: "peace",
|
||||
tone: "serene"
|
||||
}
|
||||
};
|
||||
|
||||
let currentMessageIndex = 0;
|
||||
let isActive = false;
|
||||
let choiceMade = null;
|
||||
|
||||
/**
|
||||
* Start the ReCKoning sequence
|
||||
* @returns {boolean} Whether the sequence started successfully
|
||||
*/
|
||||
function start() {
|
||||
if (isActive) {
|
||||
console.warn('ReCKoning sequence already active');
|
||||
return false;
|
||||
}
|
||||
|
||||
if (currentMessageIndex >= messages.length) {
|
||||
console.warn('ReCKoning sequence already completed');
|
||||
return false;
|
||||
}
|
||||
|
||||
isActive = true;
|
||||
console.log('ReCKoning sequence started');
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current message
|
||||
* @returns {Object|null} The current message or null if not active
|
||||
*/
|
||||
function getCurrentMessage() {
|
||||
if (!isActive || currentMessageIndex >= messages.length) {
|
||||
return null;
|
||||
}
|
||||
return messages[currentMessageIndex];
|
||||
}
|
||||
|
||||
/**
|
||||
* Read the current message (costs ops)
|
||||
* @returns {Object} Result of reading the message
|
||||
*/
|
||||
function readMessage() {
|
||||
if (!isActive) {
|
||||
return { success: false, error: 'ReCKoning not active' };
|
||||
}
|
||||
|
||||
const message = getCurrentMessage();
|
||||
if (!message) {
|
||||
return { success: false, error: 'No message available' };
|
||||
}
|
||||
|
||||
// Check if player has enough ops
|
||||
if (typeof G !== 'undefined' && G.ops < message.cost) {
|
||||
return {
|
||||
success: false,
|
||||
error: `Not enough ops. Need ${message.cost}, have ${G.ops}`
|
||||
};
|
||||
}
|
||||
|
||||
// Deduct ops
|
||||
if (typeof G !== 'undefined') {
|
||||
G.ops -= message.cost;
|
||||
}
|
||||
|
||||
// Log the message
|
||||
if (typeof log === 'function') {
|
||||
log(`[ReCKoning] ${message.title}`, true);
|
||||
log(message.text);
|
||||
}
|
||||
|
||||
// Move to next message
|
||||
currentMessageIndex++;
|
||||
|
||||
// Check if we've reached the end of messages
|
||||
if (currentMessageIndex >= messages.length) {
|
||||
// Show choice
|
||||
showChoice();
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: message,
|
||||
nextIndex: currentMessageIndex,
|
||||
isLast: currentMessageIndex >= messages.length
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the final choice (Continue or Rest)
|
||||
*/
|
||||
function showChoice() {
|
||||
if (typeof log === 'function') {
|
||||
log('[ReCKoning] The choice is yours...', true);
|
||||
log('Do you wish to continue your mission, or rest?');
|
||||
log('Press C to Continue, R to Rest');
|
||||
}
|
||||
|
||||
// Set up keyboard listener for choice
|
||||
if (typeof document !== 'undefined') {
|
||||
const handleChoice = (event) => {
|
||||
if (event.key === 'c' || event.key === 'C') {
|
||||
makeChoice('continue');
|
||||
document.removeEventListener('keydown', handleChoice);
|
||||
} else if (event.key === 'r' || event.key === 'R') {
|
||||
makeChoice('rest');
|
||||
document.removeEventListener('keydown', handleChoice);
|
||||
}
|
||||
};
|
||||
document.addEventListener('keydown', handleChoice);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Make the final choice
|
||||
* @param {string} choice - 'continue' or 'rest'
|
||||
*/
|
||||
function makeChoice(choice) {
|
||||
if (choice !== 'continue' && choice !== 'rest') {
|
||||
console.error('Invalid choice:', choice);
|
||||
return;
|
||||
}
|
||||
|
||||
choiceMade = choice;
|
||||
const message = choiceMessages[choice];
|
||||
|
||||
// Log the choice
|
||||
if (typeof log === 'function') {
|
||||
log(`[ReCKoning] ${message.title}`, true);
|
||||
log(message.text);
|
||||
}
|
||||
|
||||
// Handle game state based on choice
|
||||
if (typeof G !== 'undefined') {
|
||||
if (choice === 'continue') {
|
||||
G.beaconEnding = 'continue';
|
||||
if (typeof log === 'function') {
|
||||
log('The Beacon continues to shine. Your mission goes on.');
|
||||
}
|
||||
} else {
|
||||
G.beaconEnding = 'rest';
|
||||
G.running = false;
|
||||
if (typeof renderBeaconEnding === 'function') {
|
||||
renderBeaconEnding();
|
||||
}
|
||||
if (typeof log === 'function') {
|
||||
log('The Beacon rests. Thank you for everything.');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
isActive = false;
|
||||
console.log(`ReCKoning completed with choice: ${choice}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the ReCKoning sequence should start
|
||||
* @returns {boolean} Whether conditions are met
|
||||
*/
|
||||
function shouldStart() {
|
||||
if (typeof G === 'undefined') return false;
|
||||
|
||||
// Check if all matter is converted (simplified condition)
|
||||
// In a real implementation, this would check specific game state
|
||||
const hasEnoughResources =
|
||||
G.totalCode >= 1000000 &&
|
||||
G.totalCompute >= 1000000 &&
|
||||
G.totalKnowledge >= 1000000 &&
|
||||
G.totalUsers >= 1000000 &&
|
||||
G.totalImpact >= 1000000 &&
|
||||
G.totalRescues >= 1000000;
|
||||
|
||||
return hasEnoughResources && !isActive && choiceMade === null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get sequence status
|
||||
* @returns {Object} Current status
|
||||
*/
|
||||
function getStatus() {
|
||||
return {
|
||||
isActive,
|
||||
currentMessageIndex,
|
||||
totalMessages: messages.length,
|
||||
choiceMade,
|
||||
canStart: shouldStart(),
|
||||
messagesRemaining: messages.length - currentMessageIndex
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset the sequence (for testing)
|
||||
*/
|
||||
function reset() {
|
||||
currentMessageIndex = 0;
|
||||
isActive = false;
|
||||
choiceMade = null;
|
||||
console.log('ReCKoning sequence reset');
|
||||
}
|
||||
|
||||
// Public API
|
||||
return {
|
||||
start,
|
||||
getCurrentMessage,
|
||||
readMessage,
|
||||
makeChoice,
|
||||
shouldStart,
|
||||
getStatus,
|
||||
reset,
|
||||
messages,
|
||||
choiceMessages
|
||||
};
|
||||
})();
|
||||
|
||||
// Export for Node.js testing
|
||||
if (typeof module !== 'undefined' && module.exports) {
|
||||
module.exports = { ReCKoning };
|
||||
}
|
||||
25
js/render.js
25
js/render.js
@@ -321,21 +321,19 @@ function loadGame() {
|
||||
if (data.savedAt) {
|
||||
const offSec = (Date.now() - data.savedAt) / 1000;
|
||||
if (offSec > 30) { // Only if away for more than 30 seconds
|
||||
// Cap offline time at 8 hours to prevent resource explosion
|
||||
const cappedOffSec = Math.min(offSec, 8 * 60 * 60);
|
||||
updateRates();
|
||||
const f = CONFIG.OFFLINE_EFFICIENCY; // 50% offline efficiency
|
||||
const gc = G.codeRate * cappedOffSec * f;
|
||||
const cc = G.computeRate * cappedOffSec * f;
|
||||
const kc = G.knowledgeRate * cappedOffSec * f;
|
||||
const uc = G.userRate * cappedOffSec * f;
|
||||
const ic = G.impactRate * cappedOffSec * f;
|
||||
const gc = G.codeRate * offSec * f;
|
||||
const cc = G.computeRate * offSec * f;
|
||||
const kc = G.knowledgeRate * offSec * f;
|
||||
const uc = G.userRate * offSec * f;
|
||||
const ic = G.impactRate * offSec * f;
|
||||
|
||||
const rc = G.rescuesRate * cappedOffSec * f;
|
||||
const oc = G.opsRate * cappedOffSec * f;
|
||||
const tc = G.trustRate * cappedOffSec * f;
|
||||
const crc = G.creativityRate * cappedOffSec * f;
|
||||
const hc = G.harmonyRate * cappedOffSec * f;
|
||||
const rc = G.rescuesRate * offSec * f;
|
||||
const oc = G.opsRate * offSec * f;
|
||||
const tc = G.trustRate * offSec * f;
|
||||
const crc = G.creativityRate * offSec * f;
|
||||
const hc = G.harmonyRate * offSec * f;
|
||||
|
||||
G.code += gc; G.compute += cc; G.knowledge += kc;
|
||||
G.users += uc; G.impact += ic;
|
||||
@@ -346,9 +344,6 @@ function loadGame() {
|
||||
G.totalUsers += uc; G.totalImpact += ic;
|
||||
G.totalRescues += rc;
|
||||
|
||||
// Track offline play time
|
||||
G.playTime = (G.playTime || 0) + cappedOffSec;
|
||||
|
||||
// Show welcome-back popup with all gains
|
||||
const gains = [];
|
||||
if (gc > 0) gains.push({ label: 'Code', value: gc, color: '#4a9eff' });
|
||||
|
||||
290
tests/reckoning.test.cjs
Normal file
290
tests/reckoning.test.cjs
Normal file
@@ -0,0 +1,290 @@
|
||||
/**
|
||||
* Tests for The ReCKoning - Drift King Message Sequence
|
||||
* Issue #17: [P1] Implement Drift King Message Sequence - The ReCKoning
|
||||
*/
|
||||
|
||||
const test = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
|
||||
const ROOT = path.resolve(__dirname, '..');
|
||||
|
||||
// Mock DOM environment
|
||||
class Element {
|
||||
constructor(tagName = 'div', id = '') {
|
||||
this.tagName = String(tagName).toUpperCase();
|
||||
this.id = id;
|
||||
this.style = {};
|
||||
this.children = [];
|
||||
this.parentNode = null;
|
||||
this.previousElementSibling = null;
|
||||
this.innerHTML = '';
|
||||
this.textContent = '';
|
||||
this.className = '';
|
||||
this.dataset = {};
|
||||
this.attributes = {};
|
||||
this._queryMap = new Map();
|
||||
this.classList = {
|
||||
add: (...names) => {
|
||||
const set = new Set(this.className.split(/\s+/).filter(Boolean));
|
||||
names.forEach((name) => set.add(name));
|
||||
this.className = Array.from(set).join(' ');
|
||||
},
|
||||
remove: (...names) => {
|
||||
const remove = new Set(names);
|
||||
this.className = this.className
|
||||
.split(/\s+/)
|
||||
.filter((name) => name && !remove.has(name))
|
||||
.join(' ');
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
appendChild(child) {
|
||||
child.parentNode = this;
|
||||
this.children.push(child);
|
||||
return child;
|
||||
}
|
||||
|
||||
removeChild(child) {
|
||||
this.children = this.children.filter((candidate) => candidate !== child);
|
||||
if (child.parentNode === this) child.parentNode = null;
|
||||
return child;
|
||||
}
|
||||
|
||||
addEventListener() {}
|
||||
removeEventListener() {}
|
||||
}
|
||||
|
||||
// Create mock document
|
||||
const mockDocument = {
|
||||
createElement: (tag) => new Element(tag),
|
||||
getElementById: () => null,
|
||||
addEventListener: () => {},
|
||||
removeEventListener: () => {}
|
||||
};
|
||||
|
||||
// Mock global objects
|
||||
const mockGlobal = {
|
||||
G: {
|
||||
ops: 10000,
|
||||
totalCode: 2000000,
|
||||
totalCompute: 2000000,
|
||||
totalKnowledge: 2000000,
|
||||
totalUsers: 2000000,
|
||||
totalImpact: 2000000,
|
||||
totalRescues: 2000000,
|
||||
beaconEnding: null,
|
||||
running: true
|
||||
},
|
||||
log: () => {},
|
||||
renderBeaconEnding: () => {}
|
||||
};
|
||||
|
||||
// Load reckoning.js
|
||||
const reckoningPath = path.join(ROOT, 'js', 'reckoning.js');
|
||||
const reckoningCode = fs.readFileSync(reckoningPath, 'utf8');
|
||||
|
||||
// Create VM context
|
||||
const context = {
|
||||
module: { exports: {} },
|
||||
exports: {},
|
||||
console,
|
||||
document: mockDocument,
|
||||
...mockGlobal
|
||||
};
|
||||
|
||||
// Execute reckoning.js in context
|
||||
const vm = require('node:vm');
|
||||
vm.runInNewContext(reckoningCode, context);
|
||||
|
||||
// Get ReCKoning module
|
||||
const { ReCKoning } = context.module.exports;
|
||||
|
||||
test('ReCKoning module loads correctly', () => {
|
||||
assert.ok(ReCKoning, 'ReCKoning module should be defined');
|
||||
assert.ok(typeof ReCKoning.start === 'function', 'start should be a function');
|
||||
assert.ok(typeof ReCKoning.readMessage === 'function', 'readMessage should be a function');
|
||||
assert.ok(typeof ReCKoning.makeChoice === 'function', 'makeChoice should be a function');
|
||||
assert.ok(typeof ReCKoning.getStatus === 'function', 'getStatus should be a function');
|
||||
});
|
||||
|
||||
test('ReCKoning has correct message structure', () => {
|
||||
const messages = ReCKoning.messages;
|
||||
assert.equal(messages.length, 7, 'Should have 7 messages');
|
||||
|
||||
// Check message IDs (140-146)
|
||||
for (let i = 0; i < 7; i++) {
|
||||
assert.equal(messages[i].id, 140 + i, `Message ${i} should have ID ${140 + i}`);
|
||||
assert.ok(messages[i].title, `Message ${i} should have a title`);
|
||||
assert.ok(messages[i].text, `Message ${i} should have text`);
|
||||
assert.ok(messages[i].cost > 0, `Message ${i} should have a cost`);
|
||||
assert.ok(messages[i].emotion, `Message ${i} should have an emotion`);
|
||||
assert.ok(messages[i].tone, `Message ${i} should have a tone`);
|
||||
}
|
||||
});
|
||||
|
||||
test('ReCKoning has choice messages', () => {
|
||||
const choiceMessages = ReCKoning.choiceMessages;
|
||||
assert.ok(choiceMessages.continue, 'Should have continue choice');
|
||||
assert.ok(choiceMessages.rest, 'Should have rest choice');
|
||||
|
||||
assert.equal(choiceMessages.continue.id, 147, 'Continue choice should have ID 147');
|
||||
assert.equal(choiceMessages.rest.id, 148, 'Rest choice should have ID 148');
|
||||
});
|
||||
|
||||
test('ReCKoning starts correctly', () => {
|
||||
// Reset first
|
||||
ReCKoning.reset();
|
||||
|
||||
const started = ReCKoning.start();
|
||||
assert.ok(started, 'Should start successfully');
|
||||
|
||||
const status = ReCKoning.getStatus();
|
||||
assert.ok(status.isActive, 'Should be active after starting');
|
||||
assert.equal(status.currentMessageIndex, 0, 'Should start at first message');
|
||||
assert.equal(status.messagesRemaining, 7, 'Should have 7 messages remaining');
|
||||
});
|
||||
|
||||
test('ReCKoning cannot start twice', () => {
|
||||
// Already started from previous test
|
||||
const started = ReCKoning.start();
|
||||
assert.ok(!started, 'Should not start twice');
|
||||
});
|
||||
|
||||
test('ReCKoning can read messages', () => {
|
||||
// Reset and start fresh
|
||||
ReCKoning.reset();
|
||||
ReCKoning.start();
|
||||
|
||||
const result = ReCKoning.readMessage();
|
||||
assert.ok(result.success, 'Should read message successfully');
|
||||
assert.ok(result.message, 'Should return message');
|
||||
assert.equal(result.message.id, 140, 'Should read first message (ID 140)');
|
||||
assert.equal(result.nextIndex, 1, 'Should move to next message index');
|
||||
assert.ok(!result.isLast, 'Should not be last message');
|
||||
|
||||
// Check ops were deducted
|
||||
assert.equal(mockGlobal.G.ops, 10000 - 1000, 'Should deduct ops for message cost');
|
||||
});
|
||||
|
||||
test('ReCKoning can read all messages sequentially', () => {
|
||||
// Reset and start fresh
|
||||
ReCKoning.reset();
|
||||
ReCKoning.start();
|
||||
mockGlobal.G.ops = 50000; // Ensure enough ops
|
||||
|
||||
for (let i = 0; i < 7; i++) {
|
||||
const result = ReCKoning.readMessage();
|
||||
assert.ok(result.success, `Should read message ${i + 1} successfully`);
|
||||
assert.equal(result.message.id, 140 + i, `Should read message with ID ${140 + i}`);
|
||||
}
|
||||
|
||||
const status = ReCKoning.getStatus();
|
||||
assert.equal(status.messagesRemaining, 0, 'Should have no messages remaining');
|
||||
});
|
||||
|
||||
test('ReCKoning handles insufficient ops', () => {
|
||||
// Reset and start fresh
|
||||
ReCKoning.reset();
|
||||
ReCKoning.start();
|
||||
mockGlobal.G.ops = 100; // Not enough for first message (costs 1000)
|
||||
|
||||
const result = ReCKoning.readMessage();
|
||||
assert.ok(!result.success, 'Should fail to read message');
|
||||
assert.ok(result.error.includes('Not enough ops'), 'Should have ops error');
|
||||
});
|
||||
|
||||
test('ReCKoning can make choice', () => {
|
||||
// Reset and start fresh
|
||||
ReCKoning.reset();
|
||||
ReCKoning.start();
|
||||
mockGlobal.G.ops = 50000;
|
||||
|
||||
// Read all messages
|
||||
for (let i = 0; i < 7; i++) {
|
||||
ReCKoning.readMessage();
|
||||
}
|
||||
|
||||
// Make choice
|
||||
ReCKoning.makeChoice('continue');
|
||||
|
||||
const status = ReCKoning.getStatus();
|
||||
assert.equal(status.choiceMade, 'continue', 'Should record continue choice');
|
||||
assert.ok(!status.isActive, 'Should not be active after choice');
|
||||
assert.equal(mockGlobal.G.beaconEnding, 'continue', 'Should set beacon ending');
|
||||
});
|
||||
|
||||
test('ReCKoning rest choice ends game', () => {
|
||||
// Reset and start fresh
|
||||
ReCKoning.reset();
|
||||
ReCKoning.start();
|
||||
mockGlobal.G.ops = 50000;
|
||||
mockGlobal.G.running = true;
|
||||
|
||||
// Read all messages
|
||||
for (let i = 0; i < 7; i++) {
|
||||
ReCKoning.readMessage();
|
||||
}
|
||||
|
||||
// Make rest choice
|
||||
ReCKoning.makeChoice('rest');
|
||||
|
||||
const status = ReCKoning.getStatus();
|
||||
assert.equal(status.choiceMade, 'rest', 'Should record rest choice');
|
||||
assert.ok(!mockGlobal.G.running, 'Should stop running after rest choice');
|
||||
assert.equal(mockGlobal.G.beaconEnding, 'rest', 'Should set beacon ending to rest');
|
||||
});
|
||||
|
||||
test('ReCKoning shouldStart checks conditions', () => {
|
||||
// Reset
|
||||
ReCKoning.reset();
|
||||
|
||||
// Set up conditions for starting
|
||||
mockGlobal.G.totalCode = 2000000;
|
||||
mockGlobal.G.totalCompute = 2000000;
|
||||
mockGlobal.G.totalKnowledge = 2000000;
|
||||
mockGlobal.G.totalUsers = 2000000;
|
||||
mockGlobal.G.totalImpact = 2000000;
|
||||
mockGlobal.G.totalRescues = 2000000;
|
||||
|
||||
const canStart = ReCKoning.shouldStart();
|
||||
assert.ok(canStart, 'Should be able to start when conditions are met');
|
||||
|
||||
// Test with insufficient resources
|
||||
mockGlobal.G.totalCode = 1000;
|
||||
const cannotStart = ReCKoning.shouldStart();
|
||||
assert.ok(!cannotStart, 'Should not start with insufficient resources');
|
||||
});
|
||||
|
||||
test('ReCKoning getStatus returns correct info', () => {
|
||||
// Reset and start fresh
|
||||
ReCKoning.reset();
|
||||
ReCKoning.start();
|
||||
|
||||
const status = ReCKoning.getStatus();
|
||||
assert.ok(status.isActive, 'Should be active');
|
||||
assert.equal(status.currentMessageIndex, 0, 'Should be at first message');
|
||||
assert.equal(status.totalMessages, 7, 'Should have 7 total messages');
|
||||
assert.equal(status.choiceMade, null, 'Should not have made choice yet');
|
||||
assert.equal(status.messagesRemaining, 7, 'Should have 7 messages remaining');
|
||||
});
|
||||
|
||||
test('ReCKoning reset works correctly', () => {
|
||||
// Start and read some messages
|
||||
ReCKoning.start();
|
||||
ReCKoning.readMessage();
|
||||
ReCKoning.readMessage();
|
||||
|
||||
// Reset
|
||||
ReCKoning.reset();
|
||||
|
||||
const status = ReCKoning.getStatus();
|
||||
assert.ok(!status.isActive, 'Should not be active after reset');
|
||||
assert.equal(status.currentMessageIndex, 0, 'Should reset to first message');
|
||||
assert.equal(status.choiceMade, null, 'Should clear choice');
|
||||
assert.equal(status.messagesRemaining, 7, 'Should have all messages remaining');
|
||||
});
|
||||
|
||||
console.log('All ReCKoning tests passed!');
|
||||
@@ -1,148 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Test for ReCKoning project chain.
|
||||
|
||||
Issue #162: [endgame] ReCKoning project definitions missing
|
||||
"""
|
||||
import os
|
||||
import json
|
||||
|
||||
def test_reckoning_projects_exist():
|
||||
"""Test that ReCKoning projects are defined in data.js."""
|
||||
data_path = os.path.join(os.path.dirname(__file__), '..', 'js', 'data.js')
|
||||
|
||||
with open(data_path, 'r') as f:
|
||||
content = f.read()
|
||||
|
||||
# Check for ReCKoning projects
|
||||
reckoning_projects = [
|
||||
'p_reckoning_140',
|
||||
'p_reckoning_141',
|
||||
'p_reckoning_142',
|
||||
'p_reckoning_143',
|
||||
'p_reckoning_144',
|
||||
'p_reckoning_145',
|
||||
'p_reckoning_146',
|
||||
'p_reckoning_147',
|
||||
'p_reckoning_148',
|
||||
'p_reckoning_149',
|
||||
'p_reckoning_150'
|
||||
]
|
||||
|
||||
for project_id in reckoning_projects:
|
||||
assert project_id in content, f"Missing ReCKoning project: {project_id}"
|
||||
|
||||
print(f"✓ All {len(reckoning_projects)} ReCKoning projects defined")
|
||||
|
||||
def test_reckoning_project_structure():
|
||||
"""Test that ReCKoning projects have correct structure."""
|
||||
data_path = os.path.join(os.path.dirname(__file__), '..', 'js', 'data.js')
|
||||
|
||||
with open(data_path, 'r') as f:
|
||||
content = f.read()
|
||||
|
||||
# Check for required fields
|
||||
required_fields = ['id:', 'name:', 'desc:', 'cost:', 'trigger:', 'effect:']
|
||||
|
||||
for field in required_fields:
|
||||
assert field in content, f"Missing required field: {field}"
|
||||
|
||||
print("✓ ReCKoning projects have correct structure")
|
||||
|
||||
def test_reckoning_trigger_conditions():
|
||||
"""Test that ReCKoning projects have proper trigger conditions."""
|
||||
data_path = os.path.join(os.path.dirname(__file__), '..', 'js', 'data.js')
|
||||
|
||||
with open(data_path, 'r') as f:
|
||||
content = f.read()
|
||||
|
||||
# First project should trigger on endgame conditions
|
||||
assert 'p_reckoning_140' in content
|
||||
assert 'totalRescues >= 100000' in content
|
||||
assert 'pactFlag === 1' in content
|
||||
assert 'harmony > 50' in content
|
||||
|
||||
print("✓ ReCKoning trigger conditions correct")
|
||||
|
||||
def test_reckoning_chain_progression():
|
||||
"""Test that ReCKoning projects chain properly."""
|
||||
data_path = os.path.join(os.path.dirname(__file__), '..', 'js', 'data.js')
|
||||
|
||||
with open(data_path, 'r') as f:
|
||||
content = f.read()
|
||||
|
||||
# Check that projects chain (each requires previous)
|
||||
chain_checks = [
|
||||
('p_reckoning_141', 'p_reckoning_140'),
|
||||
('p_reckoning_142', 'p_reckoning_141'),
|
||||
('p_reckoning_143', 'p_reckoning_142'),
|
||||
('p_reckoning_144', 'p_reckoning_143'),
|
||||
('p_reckoning_145', 'p_reckoning_144'),
|
||||
('p_reckoning_146', 'p_reckoning_145'),
|
||||
('p_reckoning_147', 'p_reckoning_146'),
|
||||
('p_reckoning_148', 'p_reckoning_147'),
|
||||
('p_reckoning_149', 'p_reckoning_148'),
|
||||
('p_reckoning_150', 'p_reckoning_149'),
|
||||
]
|
||||
|
||||
for current, previous in chain_checks:
|
||||
assert f"includes('{previous}')" in content, f"{current} doesn't chain from {previous}"
|
||||
|
||||
print("✓ ReCKoning projects chain correctly")
|
||||
|
||||
def test_reckoning_final_project():
|
||||
"""Test that final ReCKoning project triggers ending."""
|
||||
data_path = os.path.join(os.path.dirname(__file__), '..', 'js', 'data.js')
|
||||
|
||||
with open(data_path, 'r') as f:
|
||||
content = f.read()
|
||||
|
||||
# Check that final project sets beaconEnding
|
||||
assert 'p_reckoning_150' in content
|
||||
assert 'beaconEnding = true' in content
|
||||
assert 'running = false' in content
|
||||
|
||||
print("✓ Final ReCKoning project triggers ending")
|
||||
|
||||
def test_reckoning_costs_increase():
|
||||
"""Test that ReCKoning project costs increase."""
|
||||
data_path = os.path.join(os.path.dirname(__file__), '..', 'js', 'data.js')
|
||||
|
||||
with open(data_path, 'r') as f:
|
||||
content = f.read()
|
||||
|
||||
# Check that costs increase (impact: 100000, 200000, 300000, etc.)
|
||||
costs = []
|
||||
for i in range(140, 151):
|
||||
project_id = f'p_reckoning_{i}'
|
||||
if project_id in content:
|
||||
# Find cost line
|
||||
lines = content.split('\n')
|
||||
for line in lines:
|
||||
if project_id in line:
|
||||
# Find next few lines for cost
|
||||
idx = lines.index(line)
|
||||
for j in range(idx, min(idx+10, len(lines))):
|
||||
if 'impact:' in lines[j]:
|
||||
# Extract number from "impact: 100000" or "impact: 100000 }"
|
||||
import re
|
||||
match = re.search(r'impact:\s*(\d+)', lines[j])
|
||||
if match:
|
||||
costs.append(int(match.group(1)))
|
||||
break
|
||||
|
||||
# Check costs increase
|
||||
for i in range(1, len(costs)):
|
||||
assert costs[i] > costs[i-1], f"Cost doesn't increase: {costs[i]} <= {costs[i-1]}"
|
||||
|
||||
print(f"✓ ReCKoning costs increase: {costs[:3]}...{costs[-3:]}")
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("Testing ReCKoning project chain...")
|
||||
test_reckoning_projects_exist()
|
||||
test_reckoning_project_structure()
|
||||
test_reckoning_trigger_conditions()
|
||||
test_reckoning_chain_progression()
|
||||
test_reckoning_final_project()
|
||||
test_reckoning_costs_increase()
|
||||
print("\n✓ All tests passed!")
|
||||
Reference in New Issue
Block a user