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(); } } }