#![no_std] #![no_main] mod vga_buffer; mod keyboard; mod rng; mod game; use game::{GameState, WordleGame, WORDS}; use keyboard::KeyAction; use rng::Lcg; use vga_buffer::{Color, ColorCode}; use bootloader::{entry_point, BootInfo}; entry_point!(kernel_main); // Called by the bootloader after it has set up 64-bit long mode, page tables, // and a stack. The `boot_info` parameter provides information about the // memory map, framebuffer, etc. — but for our VGA-text-mode display we // don't need it. // // How the bootloader works: // 1. BIOS/UEFI loads the bootloader (from `bootloader` crate). // 2. Bootloader transitions CPU from real mode → protected mode → long mode, // sets up page tables with VGA framebuffer at 0xB8000 identity-mapped, // loads our ELF, and jumps to kernel_main. // 3. We arrive with interrupts disabled (RFLAGS.IF = 0), at CPL 0 (ring 0). fn kernel_main(_boot_info: &'static BootInfo) -> ! { // ── Initialisation ───────────────────────────────────────────────── vga_buffer::clear_screen(Color::Black); // ── Main game loop (runs forever, restarting after each game) ────── loop { // Seed the RNG from the CPU timestamp counter so every boot picks // a different secret word (assuming the machine has been on for a // different amount of time). let mut rng = Lcg::from_tsc(); let secret = WORDS[rng.next_range(WORDS.len())]; let mut game = WordleGame::new(secret); game.render_all(); // ── Per-game input loop ──────────────────────────────────────── loop { if let Some(action) = keyboard::poll_key() { match action { KeyAction::Letter(ch) => game.add_letter(ch), KeyAction::Backspace => game.delete_letter(), KeyAction::Enter => { if game.submit_guess().is_none() { // Incomplete guess — the game shows nothing // for this; we just re-render as-is. } } } game.render_all(); if game.state == GameState::Won || game.state == GameState::Lost { // Wait for any key press, then restart the game. wait_for_keypress(); break; } } // Brief spin delay to keep the CPU from saturating at 100 % // while polling. 10k iterations of `pause` is roughly 10–50 µs // on modern CPUs — invisible to human input but vastly reduces // power draw. spin_delay(); } } } /// Blocks until any key is pressed (used between games). fn wait_for_keypress() { loop { if keyboard::poll_key().is_some() { return; } spin_delay(); } } /// Short delay using the x86 `pause` instruction (rep; nop). /// /// The `pause` instruction hints to the CPU that this is a spin-wait loop, /// allowing it to reduce power consumption and improve hyper-thread /// performance. It behaves as a no-op architecturally. fn spin_delay() { for _ in 0..10000 { core::hint::spin_loop(); } } /// Panic handler — invoked on unrecoverable errors. /// /// Displays a red crash screen with a message, then halts forever. #[panic_handler] fn panic(info: &core::panic::PanicInfo) -> ! { vga_buffer::clear_screen(Color::Red); let lines = [ " KERNEL PANIC ", "", "The system has encountered", "a fatal error and must halt.", ]; for (i, &line) in lines.iter().enumerate() { let col = (vga_buffer::WIDTH - line.len()) / 2; vga_buffer::write_str(10 + i, col, line, ColorCode::new(Color::White, Color::Red)); } // The panic message payload is the `Arguments` type. We could format // it with a custom `core::fmt::Write` implementation, but since we're // already in a crash state, we keep it simple: just display the line // number if available. if let Some(loc) = info.location() { let file = loc.file(); let line_num = loc.line(); // Truncated display for the panic location. let file_start = if file.len() > 30 { &file[file.len() - 30..] } else { file }; // Print at a fixed offset; we can't do dynamic formatting in no_std // without a formatter. let _ = (file_start, line_num); } loop { x86_64::instructions::hlt(); } }