Files
engfinal/kernel/src/fahrenheit.rs
T
2026-07-30 13:08:39 -04:00

460 lines
20 KiB
Rust

use crate::keyboard;
use crate::keyboard::KeyAction;
use crate::sound;
use crate::vga_buffer::{self, Color, ColorCode};
struct Choice {
text: &'static str,
score_modifier: usize,
next_node: &'static str,
}
struct NodeDef {
id: &'static str,
scene_title: Option<&'static str>,
text: &'static str,
choices: &'static [Choice],
is_terminal: bool,
}
fn get_node(id: &str) -> &'static NodeDef {
let mut i = 0;
while i < ALL_NODES.len() {
if ALL_NODES[i].id == id {
return &ALL_NODES[i];
}
i += 1;
}
&ALL_NODES[0]
}
const ALL_NODES: &[NodeDef] = &[
NodeDef {
id: "START",
scene_title: Some("Scene 1: The Old Woman's Attic"),
text: "You stand in a dimly lit attic filled with contraband books. The old woman refuses to leave her collection. Captain Beatty holds the igniter and looks at you: 'Do your duty, Montag.'",
choices: &[
Choice { text: "Burn the books and drag her out. Maintain order.", score_modifier: 10, next_node: "SCENE_2A_PARLOR" },
Choice { text: "Hesitate and secretly slip a book into your coat sleeve.", score_modifier: 25, next_node: "SCENE_2B_SUBWAY" },
Choice { text: "Drop your igniter, openly defend her, and refuse to burn history.", score_modifier: 15, next_node: "SCENE_2C_IMMEDIATE_REBELLION" },
],
is_terminal: false,
},
NodeDef {
id: "SCENE_2A_PARLOR",
scene_title: Some("Scene 2A: The Parlor Walls"),
text: "You are at home. Mildred and her friends are completely brainwashed, watching the 'White Clowns' scream on the 3-wall parlor TVs.",
choices: &[
Choice { text: "Sit down and zone out with them. Forget the attic entirely.", score_modifier: 10, next_node: "SCENE_3A_PROMOTION" },
Choice { text: "Pull out a hidden book and force them to listen to poetry.", score_modifier: 30, next_node: "SCENE_3B_TRAP" },
Choice { text: "Aggressively smash the parlor wall television screens to wake them up.", score_modifier: 15, next_node: "SCENE_3D_PARLOR_DENUNCIATION" },
],
is_terminal: false,
},
NodeDef {
id: "SCENE_2B_SUBWAY",
scene_title: Some("Scene 2B: The Sieve and the Sand"),
text: "You ride the subway with a stolen Bible. A massive audio advertisement for 'Denham's Dentifrice' blasts over the speakers, trying to numb your brain.",
choices: &[
Choice { text: "Give in to the noise. Close the book. Conformity is easier.", score_modifier: 10, next_node: "SCENE_3A_PROMOTION" },
Choice { text: "Fight the noise. Keep reading intently to memorize the text.", score_modifier: 30, next_node: "SCENE_3C_FABER_HIDE" },
Choice { text: "Stand up and scream back at the speakers, shouting scripture at the passengers.", score_modifier: 15, next_node: "SCENE_3E_SUBWAY_ARREST" },
],
is_terminal: false,
},
NodeDef {
id: "SCENE_2C_IMMEDIATE_REBELLION",
scene_title: Some("Scene 2C: The Attic Standoff"),
text: "Beatty looks at you with dangerous contempt. The old woman strikes a kitchen match herself. The attic begins to catch fire with you still inside.",
choices: &[
Choice { text: "Panic, betray your ideals, and sprint out the door with Beatty.", score_modifier: 5, next_node: "SCENE_2A_PARLOR" },
Choice { text: "Try to wrestle the match from her to save her life manually.", score_modifier: 20, next_node: "ENDING_DIE_IN_FLAMES" },
Choice { text: "Stand still in solidarity, choosing to burn alongside her and her books.", score_modifier: 25, next_node: "ENDING_MARTYR_FIRE" },
],
is_terminal: false,
},
NodeDef {
id: "SCENE_3A_PROMOTION",
scene_title: Some("Route Alpha: The Tyrant's Path"),
text: "Beatty praises your loyalty at the station and offers you a permanent promotion to Fire Captain. You have successfully killed your own curiosity.",
choices: &[
Choice { text: "Accept the promotion. Put on the Captain's helmet.", score_modifier: 10, next_node: "ENDING_TOTALITARIAN" },
Choice { text: "Vomit from guilt, reject the promotion, and run out of the station.", score_modifier: 20, next_node: "SCENE_3B_TRAP" },
Choice { text: "Accept the badge, but secretly vow to use your new power to sabotage firehouses from within.", score_modifier: 25, next_node: "ENDING_INSIDER_SABOTAGE" },
],
is_terminal: false,
},
NodeDef {
id: "SCENE_3B_TRAP",
scene_title: Some("Route Beta: The Martyr's Fire"),
text: "Mildred reports your treason. Beatty drives the Salamander to your own house and forces you to burn it. In a flash of panic, you turn the flamethrower on Beatty.",
choices: &[
Choice { text: "Flee blindly into the dark alleyways as the Mechanical Hound is deployed.", score_modifier: 20, next_node: "SCENE_4A_THE_HUNT" },
Choice { text: "Surrender to the arriving police firemen, dropping your weapon.", score_modifier: 5, next_node: "ENDING_ELIMINATED" },
Choice { text: "Stand your ground and attempt to fight off the approaching Mechanical Hound with your bare hands.", score_modifier: 15, next_node: "ENDING_HOUNDED_DEATH" },
],
is_terminal: false,
},
NodeDef {
id: "SCENE_3C_FABER_HIDE",
scene_title: Some("Route Gamma: The Intellectual Underground"),
text: "You safely reach Professor Faber's house. He hands you a small two-way communication radio 'green bullet' and tells you to run toward the river to find the Book People.",
choices: &[
Choice { text: "Dive directly into the river currents and float away from the mechanical city.", score_modifier: 30, next_node: "SCENE_4B_THE_RIVER" },
Choice { text: "Insist on staying to defend Faber from the tracker units coming for him.", score_modifier: 15, next_node: "ENDING_DEFENDING_FABER" },
Choice { text: "Lose your nerve, hide in Faber's closet, and pray they pass by.", score_modifier: 5, next_node: "ENDING_HOUNDED_DEATH" },
],
is_terminal: false,
},
NodeDef {
id: "SCENE_3D_PARLOR_DENUNCIATION",
scene_title: Some("Scene 3D: The Broken Screen"),
text: "Mildred's friends scream in absolute terror at your violent outbursts. The neighborhood alarms start sounding. You hear sirens approaching fast.",
choices: &[
Choice { text: "Sprint out the back window before the authorities encircle the block.", score_modifier: 15, next_node: "SCENE_3B_TRAP" },
Choice { text: "Sit down on the floor amid the broken glass and passively await your fate.", score_modifier: 5, next_node: "ENDING_ELIMINATED" },
Choice { text: "Force Mildred at gunpoint to flee into the night with you.", score_modifier: 10, next_node: "ENDING_TRAITOR_EXECUTION" },
],
is_terminal: false,
},
NodeDef {
id: "SCENE_3E_SUBWAY_ARREST",
scene_title: Some("Scene 3E: The Platform Corner"),
text: "Underground transit guards intercept you at the next terminal stop. You are pinned aggressively against the cold concrete tile wall. The commuters ignore you.",
choices: &[
Choice { text: "Quietly swallow an experimental chemical capsule hidden in your lapel to protect Faber's identity.", score_modifier: 25, next_node: "ENDING_SILENT_SUICIDE" },
Choice { text: "Beg for mercy and trade the location of Faber's residence for an official pardon.", score_modifier: 0, next_node: "ENDING_BETRAYER" },
Choice { text: "Break away violently and jump directly onto the electrified transit rails below.", score_modifier: 10, next_node: "ENDING_RAIL_DEATH" },
],
is_terminal: false,
},
NodeDef {
id: "SCENE_4A_THE_HUNT",
scene_title: Some("Scene 4A: The Live-Streamed Pursuit"),
text: "Helicopters illuminate the alleyways above. The entire city watches your chase live on television. You reach the edge of the dark riverbanks, your feet splashing in mud.",
choices: &[
Choice { text: "Plunge directly into the cold river to mask your scent trail from the Hound.", score_modifier: 30, next_node: "SCENE_4B_THE_RIVER" },
Choice { text: "Turn around to face the camera drones, delivering an impassioned final speech about freedom.", score_modifier: 25, next_node: "ENDING_TELEVISED_EXECUTION" },
Choice { text: "Try to cross the open highway on foot to reach the distant countryside factories.", score_modifier: 10, next_node: "ENDING_HOUNDED_DEATH" },
],
is_terminal: false,
},
NodeDef {
id: "SCENE_4B_THE_RIVER",
scene_title: Some("Scene 4B: The Gathering at the Tracks"),
text: "You emerge from the river waters far outside city limits. You discover Granger and a group of traveling, displaced academics cooking over an open fire. They invite you to pick a text to memorize.",
choices: &[
Choice { text: "Choose to preserve the Book of Ecclesiastes to give future society wisdom.", score_modifier: 35, next_node: "ENDING_RENAISSANCE" },
Choice { text: "Choose to preserve the Book of Revelation to remind them of the destructive past.", score_modifier: 35, next_node: "ENDING_RENAISSANCE" },
Choice { text: "Refuse to memorize anything, claiming humanity is an unfixable error.", score_modifier: 10, next_node: "ENDING_CYNIC_DESERTION" },
],
is_terminal: false,
},
// ── Endings ──────────────────────────────────────────────────────────
NodeDef {
id: "ENDING_TOTALITARIAN",
scene_title: None,
text: "You allowed the Dystopian Setting to conquer your individuality. Montag is dead inside, a permanent agent of the status quo.",
choices: &[],
is_terminal: true,
},
NodeDef {
id: "ENDING_RENAISSANCE",
scene_title: None,
text: "Jet bombers flatten the city. You survive downriver with the scholars as a living library, marching back to rebuild a literate civilization.",
choices: &[],
is_terminal: true,
},
NodeDef {
id: "ENDING_MARTYR_FIRE",
scene_title: None,
text: "You burn alive in the attic with the old woman. Your death becomes an unerasable legend whispered among hidden rebels.",
choices: &[],
is_terminal: true,
},
NodeDef {
id: "ENDING_DIE_IN_FLAMES",
scene_title: None,
text: "You fail to save the woman and the smoke asphyxiates you. You die in the flames, achieving nothing but an accidental casualty report.",
choices: &[],
is_terminal: true,
},
NodeDef {
id: "ENDING_HOUNDED_DEATH",
scene_title: None,
text: "The Mechanical Hound corners you. A cold procaine needle injects your leg. Your heart stops as the screen fades to cold static.",
choices: &[],
is_terminal: true,
},
NodeDef {
id: "ENDING_ELIMINATED",
scene_title: None,
text: "You are quietly escorted away to an anonymous state re-education asylum. Your records are completely scrubbed from history.",
choices: &[],
is_terminal: true,
},
NodeDef {
id: "ENDING_TELEVISED_EXECUTION",
scene_title: None,
text: "The Hound strikes you down on live television. Millions watch your final defiant monologue. You are dead, but you planted the seeds of doubt.",
choices: &[],
is_terminal: true,
},
NodeDef {
id: "ENDING_DEFENDING_FABER",
scene_title: None,
text: "You hold the door so Faber can flee out the back. You take a fatal blast from state enforcement squads, dying to preserve the old generation.",
choices: &[],
is_terminal: true,
},
NodeDef {
id: "ENDING_TRAITOR_EXECUTION",
scene_title: None,
text: "Mildred alerts an active patrol hovercraft overhead. They eliminate you both on the front lawn as public enemies.",
choices: &[],
is_terminal: true,
},
NodeDef {
id: "ENDING_SILENT_SUICIDE",
scene_title: None,
text: "You die silently in the transit terminal. The police never find Faber, and your secrets die with you.",
choices: &[],
is_terminal: true,
},
NodeDef {
id: "ENDING_BETRAYER",
scene_title: None,
text: "You save your skin but betray Faber. You are forced to watch his library burn before they execute you anyway for your crimes.",
choices: &[],
is_terminal: true,
},
NodeDef {
id: "ENDING_RAIL_DEATH",
scene_title: None,
text: "You die instantly on the high-voltage tracks. The evening news reports you as an unnamed, unhinged vagrant to avoid reporting the book.",
choices: &[],
is_terminal: true,
},
NodeDef {
id: "ENDING_INSIDER_SABOTAGE",
scene_title: None,
text: "You spend years quietly misrouting alarms and corrupting databases from within the department before being discovered and quietly eliminated.",
choices: &[],
is_terminal: true,
},
NodeDef {
id: "ENDING_CYNIC_DESERTION",
scene_title: None,
text: "You wander alone away from both the rebels and the city. When the bombs fall, you survive in the radioactive wilderness, entirely alone.",
choices: &[],
is_terminal: true,
},
];
struct FahrenheitGame {
current_node: &'static str,
score: usize,
}
fn route_label(node_id: &str) -> &'static str {
if node_id.starts_with("SCENE_3A") || node_id == "SCENE_4A_THE_HUNT" {
"Route Alpha: The Tyrant's Path"
} else if node_id == "SCENE_3B_TRAP" || node_id.starts_with("SCENE_4A") {
"Route Beta: The Martyr's Fire"
} else if node_id.starts_with("SCENE_3C") || node_id == "SCENE_4B_THE_RIVER" {
"Route Gamma: The Intellectual Underground"
} else if node_id == "SCENE_3D_PARLOR_DENUNCIATION" {
"Route Delta: The Broken Screen"
} else if node_id == "SCENE_3E_SUBWAY_ARREST" {
"Route Epsilon: The Platform Corner"
} else {
""
}
}
fn render_wrapped(start_row: usize, text: &str, color: ColorCode, max_width: usize) -> usize {
let bytes = text.as_bytes();
let len = bytes.len();
let mut row = start_row;
let mut pos = 0;
while pos < len && row < vga_buffer::HEIGHT {
let remaining = len - pos;
let line_len = if remaining <= max_width {
remaining
} else {
let mut break_pos = pos + max_width;
while break_pos > pos && bytes[break_pos] != b' ' {
break_pos -= 1;
}
if break_pos == pos {
max_width
} else {
break_pos - pos
}
};
let line = &text[pos..pos + line_len];
let line = line.trim_end();
if !line.is_empty() {
let col = (vga_buffer::WIDTH - line.len()) / 2;
vga_buffer::write_str(row, col, line, color);
}
row += 1;
pos += line_len;
if pos < len && bytes[pos] == b' ' {
pos += 1;
}
}
row
}
fn write_number(row: usize, col: usize, num: usize, color: ColorCode) -> usize {
let mut n = num;
let mut c = col;
if n == 0 {
vga_buffer::write_char(row, c, b'0', color);
return c + 1;
}
let mut digits = [b'0'; 10];
let mut len = 0;
while n > 0 {
digits[len] = b'0' + (n % 10) as u8;
n /= 10;
len += 1;
}
for i in (0..len).rev() {
vga_buffer::write_char(row, c, digits[i], color);
c += 1;
}
c
}
fn render_game(game: &FahrenheitGame) {
vga_buffer::clear_screen(Color::Black);
let node = get_node(game.current_node);
let white = ColorCode::new(Color::White, Color::Black);
let yellow = ColorCode::new(Color::Yellow, Color::Black);
let cyan = ColorCode::new(Color::Cyan, Color::Black);
let dgray = ColorCode::new(Color::DarkGray, Color::Black);
if node.is_terminal {
let title = "GAME OVER";
let col = (vga_buffer::WIDTH - title.len()) / 2;
vga_buffer::write_str(2, col, title, yellow);
let end_row = render_wrapped(5, node.text, white, 74);
let mut c = (vga_buffer::WIDTH - 24) / 2;
let score_label = "LITERARY SCORE: ";
vga_buffer::write_str(end_row + 1, c, score_label, cyan);
c += score_label.len();
c = write_number(end_row + 1, c, game.score, cyan);
vga_buffer::write_str(end_row + 1, c, "/100", cyan);
let msg = "PRESS ANY KEY TO RETURN";
let col = (vga_buffer::WIDTH - msg.len()) / 2;
vga_buffer::write_str(end_row + 3, col, msg, dgray);
} else {
if let Some(title) = node.scene_title {
let col = (vga_buffer::WIDTH - title.len()) / 2;
vga_buffer::write_str(0, col, title, yellow);
}
let text_end = render_wrapped(2, node.text, white, 74);
let mut choice_row = text_end + 1;
for i in 0..node.choices.len() {
let choice = &node.choices[i];
let prefix = match i {
0 => "1. ",
1 => "2. ",
_ => "3. ",
};
let mut buf = [0u8; 80];
let mut len = 0;
for &b in prefix.as_bytes() {
buf[len] = b;
len += 1;
}
for &b in choice.text.as_bytes() {
if len < 80 {
buf[len] = b;
len += 1;
}
}
let line = core::str::from_utf8(&buf[..len]).unwrap();
let col = (vga_buffer::WIDTH - line.len()) / 2;
vga_buffer::write_str(choice_row, col, line, white);
choice_row += 1;
}
let route = route_label(game.current_node);
if !route.is_empty() {
let sep = "----------------------------------------";
let col = (vga_buffer::WIDTH - sep.len()) / 2;
vga_buffer::write_str(choice_row + 1, col, sep, dgray);
let col = (vga_buffer::WIDTH - route.len()) / 2;
vga_buffer::write_str(choice_row + 2, col, route, cyan);
}
let score_text = "SCORE: ";
let mut c = (vga_buffer::WIDTH - 20) / 2;
vga_buffer::write_str(choice_row + 4, c, score_text, cyan);
c += score_text.len();
write_number(choice_row + 4, c, game.score, cyan);
let msg = "PRESS 1-3 TO CHOOSE ~ TO EXIT";
let col = (vga_buffer::WIDTH - msg.len()) / 2;
vga_buffer::write_str(choice_row + 6, col, msg, dgray);
}
}
pub fn run_fahrenheit() {
let mut game = FahrenheitGame {
current_node: "START",
score: 0,
};
loop {
render_game(&game);
let node = get_node(game.current_node);
if node.is_terminal {
sound::game_over_beep();
loop {
if keyboard::poll_key().is_some() {
return;
}
core::hint::spin_loop();
}
}
loop {
if let Some(action) = keyboard::poll_key() {
match action {
KeyAction::Tilde => return,
KeyAction::Digit(d) if d >= 1 && d <= 3 => {
let idx = (d - 1) as usize;
if idx < node.choices.len() {
let choice = &node.choices[idx];
game.score += choice.score_modifier;
if game.score > 100 {
game.score = 100;
}
sound::select_beep();
game.current_node = choice.next_node;
break;
}
}
_ => {}
}
}
core::hint::spin_loop();
}
}
}