Compare commits
1 Commits
fix/168-ch
...
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>
|
||||
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
15
js/main.js
15
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();
|
||||
|
||||
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 };
|
||||
}
|
||||
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!');
|
||||
Reference in New Issue
Block a user