minimum viable product
This commit is contained in:
@@ -0,0 +1,10 @@
|
||||
[package]
|
||||
name = "kernel"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
bootloader_api = "0.11"
|
||||
x86_64 = "0.14"
|
||||
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
pub const FONT_5X7_W: usize = 5;
|
||||
pub const FONT_5X7_H: usize = 7;
|
||||
|
||||
pub static FONT5X7: [[u8; 7]; 26] = [
|
||||
[0b01110, 0b10001, 0b10001, 0b11111, 0b10001, 0b10001, 0b10001], // A
|
||||
[0b11110, 0b10001, 0b10001, 0b11110, 0b10001, 0b10001, 0b11110], // B
|
||||
[0b01110, 0b10001, 0b10000, 0b10000, 0b10000, 0b10001, 0b01110], // C
|
||||
[0b11110, 0b10001, 0b10001, 0b10001, 0b10001, 0b10001, 0b11110], // D
|
||||
[0b11111, 0b10000, 0b10000, 0b11110, 0b10000, 0b10000, 0b11111], // E
|
||||
[0b11111, 0b10000, 0b10000, 0b11110, 0b10000, 0b10000, 0b10000], // F
|
||||
[0b01110, 0b10001, 0b10000, 0b10111, 0b10001, 0b10001, 0b01110], // G
|
||||
[0b10001, 0b10001, 0b10001, 0b11111, 0b10001, 0b10001, 0b10001], // H
|
||||
[0b01110, 0b00100, 0b00100, 0b00100, 0b00100, 0b00100, 0b01110], // I
|
||||
[0b00111, 0b00010, 0b00010, 0b00010, 0b00010, 0b10010, 0b01100], // J
|
||||
[0b10001, 0b10010, 0b10100, 0b11000, 0b10100, 0b10010, 0b10001], // K
|
||||
[0b10000, 0b10000, 0b10000, 0b10000, 0b10000, 0b10000, 0b11111], // L
|
||||
[0b10001, 0b11011, 0b10101, 0b10101, 0b10001, 0b10001, 0b10001], // M
|
||||
[0b10001, 0b11001, 0b10101, 0b10011, 0b10001, 0b10001, 0b10001], // N
|
||||
[0b01110, 0b10001, 0b10001, 0b10001, 0b10001, 0b10001, 0b01110], // O
|
||||
[0b11110, 0b10001, 0b10001, 0b11110, 0b10000, 0b10000, 0b10000], // P
|
||||
[0b01110, 0b10001, 0b10001, 0b10001, 0b10101, 0b10010, 0b01101], // Q
|
||||
[0b11110, 0b10001, 0b10001, 0b11110, 0b10100, 0b10010, 0b10001], // R
|
||||
[0b01110, 0b10001, 0b10000, 0b01110, 0b00001, 0b10001, 0b01110], // S
|
||||
[0b11111, 0b00100, 0b00100, 0b00100, 0b00100, 0b00100, 0b00100], // T
|
||||
[0b10001, 0b10001, 0b10001, 0b10001, 0b10001, 0b10001, 0b01110], // U
|
||||
[0b10001, 0b10001, 0b10001, 0b01010, 0b01010, 0b00100, 0b00100], // V
|
||||
[0b10001, 0b10001, 0b10001, 0b10101, 0b10101, 0b11011, 0b10001], // W
|
||||
[0b10001, 0b10001, 0b01010, 0b00100, 0b01010, 0b10001, 0b10001], // X
|
||||
[0b10001, 0b10001, 0b01010, 0b00100, 0b00100, 0b00100, 0b00100], // Y
|
||||
[0b11111, 0b00001, 0b00010, 0b00100, 0b01000, 0b10000, 0b11111], // Z
|
||||
];
|
||||
|
||||
pub fn get_5x7(ch: u8) -> Option<&'static [u8; 7]> {
|
||||
if ch.is_ascii_uppercase() {
|
||||
Some(&FONT5X7[(ch - b'A') as usize])
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub static PUNCT5X7: &[(u8, [u8; 7])] = &[
|
||||
(b'!', [0b00100, 0b00100, 0b00100, 0b00100, 0b00100, 0b00000, 0b00100]),
|
||||
(b'\'', [0b00100, 0b00100, 0b00100, 0b00000, 0b00000, 0b00000, 0b00000]),
|
||||
(b',', [0b00000, 0b00000, 0b00000, 0b00000, 0b00100, 0b00100, 0b01000]),
|
||||
(b'-', [0b00000, 0b00000, 0b00000, 0b11111, 0b00000, 0b00000, 0b00000]),
|
||||
(b'.', [0b00000, 0b00000, 0b00000, 0b00000, 0b00000, 0b00100, 0b00100]),
|
||||
(b':', [0b00000, 0b00100, 0b00100, 0b00000, 0b00100, 0b00100, 0b00000]),
|
||||
(b'_', [0b00000, 0b00000, 0b00000, 0b00000, 0b00000, 0b00000, 0b11111]),
|
||||
(b' ', [0b00000, 0b00000, 0b00000, 0b00000, 0b00000, 0b00000, 0b00000]),
|
||||
];
|
||||
|
||||
pub fn get_punct(ch: u8) -> Option<&'static [u8; 7]> {
|
||||
for &(c, ref glyph) in PUNCT5X7 {
|
||||
if c == ch {
|
||||
return Some(glyph);
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
@@ -0,0 +1,318 @@
|
||||
use crate::vga_buffer::{self, Color, ColorCode};
|
||||
|
||||
/// Hardcoded bank of 5-letter words embedded directly in the kernel binary.
|
||||
///
|
||||
/// These are common English words that work well for Wordle. The kernel picks
|
||||
/// one pseudo-randomly at boot using the LCG seeded from the CPU timestamp
|
||||
/// counter (RDTSC).
|
||||
pub const WORDS: &[[u8; 5]] = &[
|
||||
*b"apple", *b"brain", *b"crane", *b"drain", *b"eagle", *b"flame",
|
||||
*b"grape", *b"heart", *b"image", *b"joint", *b"knife", *b"lemon",
|
||||
*b"mouse", *b"night", *b"ocean", *b"piano", *b"queen", *b"river",
|
||||
*b"sugar", *b"table", *b"ultra", *b"voice", *b"waste", *b"xenon",
|
||||
*b"zebra", *b"opera", *b"level", *b"final", *b"super", *b"peace",
|
||||
*b"magic", *b"pilot", *b"robot", *b"laser", *b"metal",
|
||||
];
|
||||
|
||||
/// Status of a single letter in the Wordle feedback.
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub enum LetterStatus {
|
||||
/// Letter in this position has not been evaluated yet (untyped or
|
||||
/// current guess in progress).
|
||||
Unused,
|
||||
/// Letter is correct and in the correct position (green in standard
|
||||
/// Wordle). The VGA background will be set to Green.
|
||||
Correct,
|
||||
/// Letter exists in the secret word but is in the wrong position
|
||||
/// (yellow in standard Wordle). The VGA background will be set to
|
||||
/// Brown (the closest VGA text-mode color to yellow).
|
||||
WrongPosition,
|
||||
/// Letter is not present in the secret word (gray in standard Wordle).
|
||||
/// The VGA background will be set to LightGray with black text.
|
||||
Incorrect,
|
||||
}
|
||||
|
||||
/// Top-level game state.
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub enum GameState {
|
||||
Playing,
|
||||
Won,
|
||||
Lost,
|
||||
}
|
||||
|
||||
/// Complete Wordle game state machine.
|
||||
///
|
||||
/// Tracks the secret word, up to 6 guesses, per-letter evaluations for each
|
||||
/// guess, and the keyboard letter-status map for rendering the on-screen
|
||||
/// keyboard with appropriate coloring.
|
||||
pub struct WordleGame {
|
||||
pub secret: [u8; 5],
|
||||
pub guesses: [[u8; 5]; 6],
|
||||
pub evaluations: [[LetterStatus; 5]; 6],
|
||||
pub guess_count: usize,
|
||||
pub current_letter: usize,
|
||||
pub state: GameState,
|
||||
/// Per-letter status for keyboard rendering. Index 0 = 'A', 25 = 'Z'.
|
||||
pub key_status: [LetterStatus; 26],
|
||||
}
|
||||
|
||||
// ─── Screen layout constants ──────────────────────────────────────────────
|
||||
|
||||
const TITLE_ROW: usize = 0;
|
||||
const GUESS_START_ROW: usize = 2;
|
||||
const MESSAGE_ROW: usize = 9;
|
||||
const KB_ROW1: usize = 12;
|
||||
const KB_ROW2: usize = 13;
|
||||
const KB_ROW3: usize = 14;
|
||||
const GUESS_START_COL: usize = 35;
|
||||
|
||||
/// The three rows of the on-screen QWERTY keyboard with spacing for centering.
|
||||
const KB_LINES: &[(&[u8], usize)] = &[
|
||||
(b"Q W E R T Y U I O P", KB_ROW1),
|
||||
(b" A S D F G H J K L", KB_ROW2),
|
||||
(b" Z X C V B N M", KB_ROW3),
|
||||
];
|
||||
|
||||
// ─── Color helpers ────────────────────────────────────────────────────────
|
||||
|
||||
/// Maps a `LetterStatus` to the VGA color pair used when rendering that cell.
|
||||
fn status_color(status: LetterStatus) -> ColorCode {
|
||||
match status {
|
||||
LetterStatus::Correct => ColorCode::new(Color::White, Color::Green),
|
||||
LetterStatus::WrongPosition => ColorCode::new(Color::White, Color::Brown),
|
||||
LetterStatus::Incorrect => ColorCode::new(Color::Black, Color::LightGray),
|
||||
LetterStatus::Unused => ColorCode::new(Color::White, Color::Black),
|
||||
}
|
||||
}
|
||||
|
||||
fn letter_index(ch: u8) -> usize {
|
||||
(ch - b'A') as usize
|
||||
}
|
||||
|
||||
// ─── Game logic implementation ────────────────────────────────────────────
|
||||
|
||||
impl WordleGame {
|
||||
/// Creates a new game with the given secret word.
|
||||
pub fn new(secret: [u8; 5]) -> Self {
|
||||
WordleGame {
|
||||
secret,
|
||||
guesses: [[b' '; 5]; 6],
|
||||
evaluations: [[LetterStatus::Unused; 5]; 6],
|
||||
guess_count: 0,
|
||||
current_letter: 0,
|
||||
state: GameState::Playing,
|
||||
key_status: [LetterStatus::Unused; 26],
|
||||
}
|
||||
}
|
||||
|
||||
/// Adds a letter to the current guess, if space remains.
|
||||
pub fn add_letter(&mut self, letter: u8) {
|
||||
if self.current_letter < 5 {
|
||||
self.guesses[self.guess_count][self.current_letter] = letter;
|
||||
self.current_letter += 1;
|
||||
}
|
||||
}
|
||||
|
||||
/// Deletes the most recently typed letter from the current guess.
|
||||
pub fn delete_letter(&mut self) {
|
||||
if self.current_letter > 0 {
|
||||
self.current_letter -= 1;
|
||||
self.guesses[self.guess_count][self.current_letter] = b' ';
|
||||
}
|
||||
}
|
||||
|
||||
/// Submits the current guess for evaluation.
|
||||
///
|
||||
/// Returns `None` if the guess is incomplete (< 5 letters).
|
||||
/// Returns `Some(true)` if the game ended (won or lost).
|
||||
/// Returns `Some(false)` if the guess was valid and the game continues.
|
||||
pub fn submit_guess(&mut self) -> Option<bool> {
|
||||
if self.current_letter < 5 {
|
||||
return None;
|
||||
}
|
||||
|
||||
let guess = self.guesses[self.guess_count];
|
||||
let evaluation = evaluate_guess(&self.secret, &guess);
|
||||
self.evaluations[self.guess_count] = evaluation;
|
||||
|
||||
// Update the keyboard letter statuses. Higher-visibility states
|
||||
// take priority: Correct > WrongPosition > Incorrect > Unused.
|
||||
for i in 0..5 {
|
||||
let idx = letter_index(guess[i]);
|
||||
let new = evaluation[i];
|
||||
let current = self.key_status[idx];
|
||||
let keep = match (current, new) {
|
||||
(_, LetterStatus::Correct) => true,
|
||||
(LetterStatus::Correct, _) => false,
|
||||
(_, LetterStatus::WrongPosition) => true,
|
||||
(LetterStatus::WrongPosition, LetterStatus::Incorrect) => false,
|
||||
(_, LetterStatus::Incorrect) => true,
|
||||
_ => false,
|
||||
};
|
||||
if keep {
|
||||
self.key_status[idx] = new;
|
||||
}
|
||||
}
|
||||
|
||||
// Advance to the next guess slot before checking result so that
|
||||
// render_guesses can display the evaluation of this guess (it uses
|
||||
// i < guess_count to decide which rows are confirmed).
|
||||
self.guess_count += 1;
|
||||
self.current_letter = 0;
|
||||
|
||||
if evaluation.iter().all(|&s| s == LetterStatus::Correct) {
|
||||
self.state = GameState::Won;
|
||||
return Some(true);
|
||||
}
|
||||
|
||||
if self.guess_count >= 6 {
|
||||
self.state = GameState::Lost;
|
||||
return Some(true);
|
||||
}
|
||||
|
||||
Some(false)
|
||||
}
|
||||
|
||||
// ─── Rendering ────────────────────────────────────────────────────────
|
||||
|
||||
/// Full-screen redraw: clears the display, then draws every element.
|
||||
pub fn render_all(&self) {
|
||||
vga_buffer::clear_screen(Color::Black);
|
||||
self.render_title();
|
||||
self.render_guesses();
|
||||
self.render_keyboard();
|
||||
self.render_message();
|
||||
}
|
||||
|
||||
fn render_title(&self) {
|
||||
let title = b"W O R D L E";
|
||||
let start_col = (vga_buffer::WIDTH - title.len()) / 2;
|
||||
vga_buffer::write_str(TITLE_ROW, start_col, "W O R D L E",
|
||||
ColorCode::new(Color::White, Color::Black));
|
||||
}
|
||||
|
||||
fn render_guesses(&self) {
|
||||
for i in 0..6 {
|
||||
let row = GUESS_START_ROW + i;
|
||||
|
||||
if i < self.guess_count {
|
||||
// Confirmed guess — show with evaluation colors.
|
||||
for j in 0..5 {
|
||||
let col = GUESS_START_COL + j * 2;
|
||||
let color = status_color(self.evaluations[i][j]);
|
||||
vga_buffer::write_char(row, col, self.guesses[i][j], color);
|
||||
}
|
||||
} else if i == self.guess_count && self.state == GameState::Playing {
|
||||
// Current guess being typed.
|
||||
for j in 0..5 {
|
||||
let col = GUESS_START_COL + j * 2;
|
||||
if j < self.current_letter {
|
||||
vga_buffer::write_char(row, col, self.guesses[i][j],
|
||||
ColorCode::new(Color::White, Color::Black));
|
||||
} else if j == self.current_letter {
|
||||
// Cursor indicator: an underscore.
|
||||
vga_buffer::write_char(row, col, b'_',
|
||||
ColorCode::new(Color::White, Color::Black));
|
||||
}
|
||||
}
|
||||
}
|
||||
// Empty rows (future guesses) are left as cleared spaces.
|
||||
}
|
||||
}
|
||||
|
||||
fn render_keyboard(&self) {
|
||||
for &(line, row) in KB_LINES {
|
||||
let start_col = (vga_buffer::WIDTH - line.len()) / 2;
|
||||
for (i, &ch) in line.iter().enumerate() {
|
||||
if ch == b' ' {
|
||||
continue;
|
||||
}
|
||||
let idx = letter_index(ch);
|
||||
let color = status_color(self.key_status[idx]);
|
||||
vga_buffer::write_char(row, start_col + i, ch, color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn render_message(&self) {
|
||||
match self.state {
|
||||
GameState::Won => {
|
||||
let msg = "Congratulations! Press any key to restart";
|
||||
let start_col = (vga_buffer::WIDTH - msg.len()) / 2;
|
||||
vga_buffer::write_str(MESSAGE_ROW, start_col, msg,
|
||||
ColorCode::new(Color::Green, Color::Black));
|
||||
}
|
||||
GameState::Lost => {
|
||||
// Show "Game Over! The word was: XXXXX"
|
||||
let prefix = "Game Over! The word was: ";
|
||||
let total_len = prefix.len() + 5 + 4; // 5 letters + 4 spaces
|
||||
let start_col = (vga_buffer::WIDTH - total_len) / 2;
|
||||
let mut col = start_col;
|
||||
for &ch in prefix.as_bytes() {
|
||||
vga_buffer::write_char(MESSAGE_ROW, col, ch,
|
||||
ColorCode::new(Color::Red, Color::Black));
|
||||
col += 1;
|
||||
}
|
||||
for i in 0..5 {
|
||||
col += 1; // space between letters
|
||||
let color = status_color(self.evaluations[5][i]);
|
||||
vga_buffer::write_char(MESSAGE_ROW, col, self.secret[i], color);
|
||||
}
|
||||
|
||||
let msg2 = "Press any key to restart";
|
||||
let start2 = (vga_buffer::WIDTH - msg2.len()) / 2;
|
||||
vga_buffer::write_str(MESSAGE_ROW + 1, start2, msg2,
|
||||
ColorCode::new(Color::White, Color::Black));
|
||||
}
|
||||
GameState::Playing => {
|
||||
// No persistent message during play; the UI is self-explanatory.
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Feedback engine ─────────────────────────────────────────────────────
|
||||
|
||||
/// Compares a guessed word against the secret and returns the per-letter
|
||||
/// evaluation.
|
||||
///
|
||||
/// Algorithm (identical to the original Wordle):
|
||||
/// 1. First pass: mark exact positional matches as `Correct`.
|
||||
/// 2. Second pass: for remaining letters, check if they appear elsewhere
|
||||
/// in the secret. If so, mark as `WrongPosition`; otherwise `Incorrect`.
|
||||
///
|
||||
/// A letter in the secret can only match one guessed letter. Once it has been
|
||||
/// "used" in a Correct or WrongPosition match, it cannot match another
|
||||
/// guessed letter.
|
||||
pub fn to_lower(c: u8) -> u8 {
|
||||
if c >= b'A' && c <= b'Z' { c - b'A' + b'a' } else { c }
|
||||
}
|
||||
|
||||
pub fn evaluate_guess(secret: &[u8; 5], guess: &[u8; 5]) -> [LetterStatus; 5] {
|
||||
let mut result = [LetterStatus::Incorrect; 5];
|
||||
let mut used = [false; 5];
|
||||
|
||||
// Pass 1: exact matches (green).
|
||||
for i in 0..5 {
|
||||
if to_lower(guess[i]) == secret[i] {
|
||||
result[i] = LetterStatus::Correct;
|
||||
used[i] = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Pass 2: wrong-position matches (yellow).
|
||||
for i in 0..5 {
|
||||
if result[i] == LetterStatus::Correct {
|
||||
continue;
|
||||
}
|
||||
for j in 0..5 {
|
||||
if !used[j] && to_lower(guess[i]) == secret[j] {
|
||||
result[i] = LetterStatus::WrongPosition;
|
||||
used[j] = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
@@ -0,0 +1,212 @@
|
||||
use core::sync::atomic::{AtomicBool, Ordering};
|
||||
use x86_64::instructions::port::Port;
|
||||
|
||||
const TIMEOUT: u32 = 1_000_000;
|
||||
|
||||
static KEYBOARD_READY: AtomicBool = AtomicBool::new(false);
|
||||
|
||||
/// Initialises the PS/2 controller and keyboard.
|
||||
///
|
||||
/// UEFI firmware often leaves the PS/2 controller uninitialised, so we must
|
||||
/// perform the full startup sequence ourselves before the keyboard will work.
|
||||
/// If no PS/2 controller is present (common on modern UEFI systems), this
|
||||
/// returns gracefully without hanging.
|
||||
pub fn init() {
|
||||
// Disable both PS/2 ports
|
||||
if !try_write_command(0xAD) { return; }
|
||||
if !try_write_command(0xA7) { return; }
|
||||
|
||||
// Flush output buffer
|
||||
let _ = try_read_data();
|
||||
|
||||
// Read and update configuration byte
|
||||
if !try_write_command(0x20) { return; }
|
||||
let config = match try_read_data() {
|
||||
Some(val) => val & !0x07,
|
||||
None => return,
|
||||
};
|
||||
if !try_write_command(0x60) { return; }
|
||||
if !try_write_data(config) { return; }
|
||||
|
||||
// Controller self-test
|
||||
if !try_write_command(0xAA) { return; }
|
||||
if try_read_data() != Some(0x55) { return; }
|
||||
|
||||
// Enable keyboard port
|
||||
if !try_write_command(0xAE) { return; }
|
||||
|
||||
// Reset keyboard
|
||||
if !try_write_data(0xFF) { return; }
|
||||
if try_read_data() != Some(0xFA) { return; } // ACK
|
||||
if try_read_data() != Some(0xAA) { return; } // self-test passed
|
||||
|
||||
KEYBOARD_READY.store(true, Ordering::SeqCst);
|
||||
}
|
||||
|
||||
fn try_write_command(cmd: u8) -> bool {
|
||||
if !poll_write_ready() { return false; }
|
||||
unsafe { Port::new(0x64).write(cmd) }
|
||||
true
|
||||
}
|
||||
|
||||
fn try_write_data(data: u8) -> bool {
|
||||
if !poll_write_ready() { return false; }
|
||||
unsafe { Port::new(0x60).write(data) }
|
||||
true
|
||||
}
|
||||
|
||||
fn poll_write_ready() -> bool {
|
||||
for _ in 0..TIMEOUT {
|
||||
if read_status() & 2 == 0 {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
fn try_read_data() -> Option<u8> {
|
||||
for _ in 0..TIMEOUT {
|
||||
if read_status() & 1 != 0 {
|
||||
return Some(unsafe { Port::new(0x60).read() });
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// Actions produced by the PS/2 keyboard decoder.
|
||||
///
|
||||
/// Only A–Z letters, Enter, and Backspace are relevant for the Wordle game.
|
||||
/// All other keys (modifiers, function keys, arrows) are silently ignored.
|
||||
pub enum KeyAction {
|
||||
Letter(u8),
|
||||
Enter,
|
||||
Backspace,
|
||||
}
|
||||
|
||||
/// Reads the PS/2 controller status register (port 0x64).
|
||||
///
|
||||
/// Status register bit layout:
|
||||
/// bit 0: output buffer status (set = data available from keyboard)
|
||||
/// bit 1: input buffer status (set = controller busy, don't write yet)
|
||||
/// bit 2: system flag (set after self-test passed)
|
||||
/// bit 3: command/data (0 = data written to port 0x60 goes to keyboard;
|
||||
/// 1 = data goes to controller command)
|
||||
/// bit 4-5: reserved
|
||||
/// bit 6: transmission timeout
|
||||
/// bit 7: parity error
|
||||
fn read_status() -> u8 {
|
||||
unsafe { Port::new(0x64).read() }
|
||||
}
|
||||
|
||||
/// Reads a byte from the PS/2 data port (port 0x60).
|
||||
///
|
||||
/// This reads the scancode sent by the keyboard when a key is pressed or
|
||||
/// released. Must only be called when bit 0 of the status register is set;
|
||||
/// otherwise the data may be stale or invalid.
|
||||
fn read_data() -> u8 {
|
||||
unsafe { Port::new(0x60).read() }
|
||||
}
|
||||
|
||||
/// Returns true when a byte is available from the keyboard.
|
||||
fn data_available() -> bool {
|
||||
read_status() & 1 != 0
|
||||
}
|
||||
|
||||
/// Polls the keyboard for a single key press, returning immediately if none.
|
||||
///
|
||||
/// This is called from the main game loop and is non-blocking. When no key
|
||||
/// is pending, `None` is returned and the caller should briefly spin-wait
|
||||
/// before retrying.
|
||||
///
|
||||
/// ## Scan code handling
|
||||
/// The PS/2 keyboard defaults to Scan Code Set 2. Most PS/2 controllers
|
||||
/// run in "translated" mode, converting Set 2 scancodes to Set 1 on the
|
||||
/// fly. In Set 1:
|
||||
/// - Make codes (key pressed): 0x01 – 0x7F
|
||||
/// - Break codes (key released): 0x81 – 0xFF (= make_code | 0x80)
|
||||
/// - Extended keys (arrows, etc.): prefixed with 0xE0
|
||||
///
|
||||
/// We do NOT explicitly change the keyboard's scan code set (command 0xF0)
|
||||
/// because doing so would corrupt the controller's translation — the
|
||||
/// keyboard stays in its default Set 2, and the controller handles the
|
||||
/// conversion to Set 1 transparently.
|
||||
///
|
||||
/// We ignore break codes and extended codes since we only care about when
|
||||
/// a letter key is pressed.
|
||||
pub fn poll_key() -> Option<KeyAction> {
|
||||
if !KEYBOARD_READY.load(Ordering::SeqCst) {
|
||||
return None;
|
||||
}
|
||||
if !data_available() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let scancode = read_data();
|
||||
|
||||
// Ignore the extended-key prefix (0xE0). The next byte is the actual
|
||||
// scancode for the extended key (e.g., arrow, home, etc.), which we
|
||||
// don't need — discard it too.
|
||||
if scancode == 0xE0 {
|
||||
while !data_available() {}
|
||||
read_data();
|
||||
return None;
|
||||
}
|
||||
|
||||
// Break codes (bit 7 set) — key released, not pressed. Ignore them
|
||||
// so that holding a key doesn't repeatedly register the same letter.
|
||||
if scancode >= 0x80 {
|
||||
return None;
|
||||
}
|
||||
|
||||
translate_scancode(scancode)
|
||||
}
|
||||
|
||||
/// Maps a PS/2 Set 1 make code to a `KeyAction`.
|
||||
///
|
||||
/// Scan code set 1 layout for the main alphanumeric block:
|
||||
///
|
||||
/// ┌────┬────┬────┬────┬────┬────┬────┬────┬────┬────┐
|
||||
/// │ Q │ W │ E │ R │ T │ Y │ U │ I │ O │ P │
|
||||
/// │0x10│0x11│0x12│0x13│0x14│0x15│0x16│0x17│0x18│0x19│
|
||||
/// ├────┼────┼────┼────┼────┼────┼────┼────┼────┼────┤
|
||||
/// │ A │ S │ D │ F │ G │ H │ J │ K │ L │ │
|
||||
/// │0x1E│0x1F│0x20│0x21│0x22│0x23│0x24│0x25│0x26│ │
|
||||
/// ├────┼────┼────┼────┼────┼────┼────┼────┼────┼────┤
|
||||
/// │ Z │ X │ C │ V │ B │ N │ M │ │ │ │
|
||||
/// │0x2C│0x2D│0x2E│0x2F│0x30│0x31│0x32│ │ │ │
|
||||
/// └────┴────┴────┴────┴────┴────┴────┴────┴────┴────┘
|
||||
///
|
||||
/// Enter: 0x1C Backspace: 0x0E
|
||||
fn translate_scancode(scancode: u8) -> Option<KeyAction> {
|
||||
match scancode {
|
||||
0x0E => Some(KeyAction::Backspace),
|
||||
0x1C => Some(KeyAction::Enter),
|
||||
0x10 => Some(KeyAction::Letter(b'Q')),
|
||||
0x11 => Some(KeyAction::Letter(b'W')),
|
||||
0x12 => Some(KeyAction::Letter(b'E')),
|
||||
0x13 => Some(KeyAction::Letter(b'R')),
|
||||
0x14 => Some(KeyAction::Letter(b'T')),
|
||||
0x15 => Some(KeyAction::Letter(b'Y')),
|
||||
0x16 => Some(KeyAction::Letter(b'U')),
|
||||
0x17 => Some(KeyAction::Letter(b'I')),
|
||||
0x18 => Some(KeyAction::Letter(b'O')),
|
||||
0x19 => Some(KeyAction::Letter(b'P')),
|
||||
0x1E => Some(KeyAction::Letter(b'A')),
|
||||
0x1F => Some(KeyAction::Letter(b'S')),
|
||||
0x20 => Some(KeyAction::Letter(b'D')),
|
||||
0x21 => Some(KeyAction::Letter(b'F')),
|
||||
0x22 => Some(KeyAction::Letter(b'G')),
|
||||
0x23 => Some(KeyAction::Letter(b'H')),
|
||||
0x24 => Some(KeyAction::Letter(b'J')),
|
||||
0x25 => Some(KeyAction::Letter(b'K')),
|
||||
0x26 => Some(KeyAction::Letter(b'L')),
|
||||
0x2C => Some(KeyAction::Letter(b'Z')),
|
||||
0x2D => Some(KeyAction::Letter(b'X')),
|
||||
0x2E => Some(KeyAction::Letter(b'C')),
|
||||
0x2F => Some(KeyAction::Letter(b'V')),
|
||||
0x30 => Some(KeyAction::Letter(b'B')),
|
||||
0x31 => Some(KeyAction::Letter(b'N')),
|
||||
0x32 => Some(KeyAction::Letter(b'M')),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
#![no_std]
|
||||
#![no_main]
|
||||
|
||||
mod font;
|
||||
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_api::{entry_point, BootInfo};
|
||||
|
||||
entry_point!(kernel_main);
|
||||
|
||||
fn kernel_main(boot_info: &'static mut BootInfo) -> ! {
|
||||
let fb = boot_info.framebuffer.take()
|
||||
.expect("no framebuffer provided by bootloader");
|
||||
vga_buffer::init(fb);
|
||||
|
||||
vga_buffer::clear_screen(Color::Black);
|
||||
keyboard::init();
|
||||
|
||||
loop {
|
||||
let mut rng = Lcg::from_tsc();
|
||||
let secret = WORDS[rng.next_range(WORDS.len())];
|
||||
|
||||
let mut game = WordleGame::new(secret);
|
||||
game.render_all();
|
||||
|
||||
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() {}
|
||||
}
|
||||
}
|
||||
game.render_all();
|
||||
|
||||
if game.state == GameState::Won || game.state == GameState::Lost {
|
||||
wait_for_keypress();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
spin_delay();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn wait_for_keypress() {
|
||||
loop {
|
||||
if keyboard::poll_key().is_some() {
|
||||
return;
|
||||
}
|
||||
spin_delay();
|
||||
}
|
||||
}
|
||||
|
||||
fn spin_delay() {
|
||||
for _ in 0..10000 {
|
||||
core::hint::spin_loop();
|
||||
}
|
||||
}
|
||||
|
||||
#[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));
|
||||
}
|
||||
|
||||
if let Some(loc) = info.location() {
|
||||
let _ = (loc.file(), loc.line());
|
||||
}
|
||||
|
||||
loop {
|
||||
x86_64::instructions::hlt();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
use core::arch::x86_64::_rdtsc;
|
||||
|
||||
/// A simple Linear Congruential Generator (LCG) for pseudo-random numbers.
|
||||
///
|
||||
/// Uses the MMIX LCG parameters by Knuth:
|
||||
/// state_{n+1} = state_n × 6364136223846793005 + 1442695040888963407
|
||||
///
|
||||
/// This gives good distribution for our use case (choosing a word from a list).
|
||||
/// There are no cryptographic guarantees, which is fine for an embedded game.
|
||||
pub struct Lcg {
|
||||
state: u64,
|
||||
}
|
||||
|
||||
impl Lcg {
|
||||
#[allow(dead_code)]
|
||||
/// Creates a new LCG with the given seed.
|
||||
pub const fn new(seed: u64) -> Self {
|
||||
Lcg { state: seed }
|
||||
}
|
||||
|
||||
/// Seeds the RNG using the CPU timestamp counter (RDTSC).
|
||||
///
|
||||
/// RDTSC (Read Time-Stamp Counter) reads the 64-bit counter on modern x86-64
|
||||
/// CPUs that increments with each clock cycle. Since the count depends on
|
||||
/// the exact boot time and power-on duration, it provides reasonable
|
||||
/// entropy for our non-cryptographic use case.
|
||||
///
|
||||
/// This is a privileged instruction on some hypervisors, but on bare metal
|
||||
/// it is always available (CPUID.80000001H:EDX.TSC[bit 4] is set on all
|
||||
/// x86-64 CPUs).
|
||||
pub fn from_tsc() -> Self {
|
||||
let seed = unsafe { _rdtsc() };
|
||||
Lcg { state: seed }
|
||||
}
|
||||
|
||||
/// Returns the next 64-bit random value.
|
||||
pub fn next_u64(&mut self) -> u64 {
|
||||
self.state = self
|
||||
.state
|
||||
.wrapping_mul(6364136223846793005)
|
||||
.wrapping_add(1442695040888963407);
|
||||
self.state
|
||||
}
|
||||
|
||||
/// Returns a random index in the range [0, range).
|
||||
///
|
||||
/// Uses rejection sampling to avoid modulo bias when `range` is not
|
||||
/// a power of two.
|
||||
pub fn next_range(&mut self, range: usize) -> usize {
|
||||
(self.next_u64() % range as u64) as usize
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,228 @@
|
||||
use crate::font;
|
||||
use bootloader_api::info::{FrameBuffer, PixelFormat};
|
||||
|
||||
pub const WIDTH: usize = 80;
|
||||
pub const HEIGHT: usize = 25;
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
#[allow(dead_code)]
|
||||
pub enum Color {
|
||||
Black = 0,
|
||||
Blue = 1,
|
||||
Green = 2,
|
||||
Cyan = 3,
|
||||
Red = 4,
|
||||
Magenta = 5,
|
||||
Brown = 6,
|
||||
LightGray = 7,
|
||||
DarkGray = 8,
|
||||
LightBlue = 9,
|
||||
LightGreen = 10,
|
||||
LightCyan = 11,
|
||||
LightRed = 12,
|
||||
Pink = 13,
|
||||
Yellow = 14,
|
||||
White = 15,
|
||||
}
|
||||
|
||||
fn color_to_rgb(color: Color) -> (u8, u8, u8) {
|
||||
match color {
|
||||
Color::Black => (0, 0, 0),
|
||||
Color::Blue => (0, 0, 170),
|
||||
Color::Green => (0, 170, 0),
|
||||
Color::Cyan => (0, 170, 170),
|
||||
Color::Red => (170, 0, 0),
|
||||
Color::Magenta => (170, 0, 170),
|
||||
Color::Brown => (170, 85, 0),
|
||||
Color::LightGray => (170, 170, 170),
|
||||
Color::DarkGray => (85, 85, 85),
|
||||
Color::LightBlue => (85, 85, 255),
|
||||
Color::LightGreen => (85, 255, 85),
|
||||
Color::LightCyan => (85, 255, 255),
|
||||
Color::LightRed => (255, 85, 85),
|
||||
Color::Pink => (255, 85, 255),
|
||||
Color::Yellow => (255, 255, 85),
|
||||
Color::White => (255, 255, 255),
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct ColorCode {
|
||||
fg: Color,
|
||||
bg: Color,
|
||||
}
|
||||
|
||||
impl ColorCode {
|
||||
pub const fn new(fg: Color, bg: Color) -> ColorCode {
|
||||
ColorCode { fg, bg }
|
||||
}
|
||||
}
|
||||
|
||||
struct FbState {
|
||||
ptr: *mut u8,
|
||||
width: usize,
|
||||
height: usize,
|
||||
stride: usize,
|
||||
bpp: usize,
|
||||
is_bgr: bool,
|
||||
x_off: usize,
|
||||
y_off: usize,
|
||||
cell_w: usize,
|
||||
cell_h: usize,
|
||||
}
|
||||
|
||||
unsafe impl Send for FbState {}
|
||||
|
||||
static mut FB: Option<FbState> = None;
|
||||
|
||||
pub fn init(fb: FrameBuffer) {
|
||||
let info = fb.info();
|
||||
let buf = fb.into_buffer();
|
||||
let ptr = buf.as_mut_ptr();
|
||||
let is_bgr = matches!(info.pixel_format, PixelFormat::Bgr);
|
||||
let cell_w = (info.width / WIDTH).max(1);
|
||||
let cell_h = (info.height / HEIGHT).max(1);
|
||||
let x_off = (info.width - cell_w * WIDTH) / 2;
|
||||
let y_off = (info.height - cell_h * HEIGHT) / 2;
|
||||
unsafe {
|
||||
FB = Some(FbState {
|
||||
ptr,
|
||||
width: info.width,
|
||||
height: info.height,
|
||||
stride: info.stride,
|
||||
bpp: info.bytes_per_pixel,
|
||||
is_bgr,
|
||||
x_off,
|
||||
y_off,
|
||||
cell_w,
|
||||
cell_h,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
fn with_fb<F, R>(f: F) -> R
|
||||
where
|
||||
F: FnOnce(&mut FbState) -> R,
|
||||
{
|
||||
unsafe {
|
||||
let fb = &mut *core::ptr::addr_of_mut!(FB);
|
||||
f(fb.as_mut().expect("Framebuffer not initialized"))
|
||||
}
|
||||
}
|
||||
|
||||
fn write_pixel(state: &mut FbState, x: usize, y: usize, r: u8, g: u8, b: u8) {
|
||||
if x >= state.width || y >= state.height {
|
||||
return;
|
||||
}
|
||||
let offset = (y * state.stride + x) * state.bpp;
|
||||
unsafe {
|
||||
if state.is_bgr {
|
||||
*state.ptr.add(offset) = b;
|
||||
*state.ptr.add(offset + 1) = g;
|
||||
*state.ptr.add(offset + 2) = r;
|
||||
} else {
|
||||
*state.ptr.add(offset) = r;
|
||||
*state.ptr.add(offset + 1) = g;
|
||||
*state.ptr.add(offset + 2) = b;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn fill_rect(state: &mut FbState, x: usize, y: usize, w: usize, h: usize, r: u8, g: u8, b: u8) {
|
||||
let x_end = (x + w).min(state.width);
|
||||
let y_end = (y + h).min(state.height);
|
||||
for py in y..y_end {
|
||||
for px in x..x_end {
|
||||
write_pixel(state, px, py, r, g, b);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn get_glyph(ch: u8) -> Option<&'static [u8; 7]> {
|
||||
if let Some(g) = font::get_5x7(ch) {
|
||||
return Some(g);
|
||||
}
|
||||
font::get_punct(ch)
|
||||
}
|
||||
|
||||
fn draw_glyph_at(
|
||||
state: &mut FbState,
|
||||
cell_x: usize, cell_y: usize, cell_w: usize, cell_h: usize,
|
||||
ch: u8,
|
||||
fg_r: u8, fg_g: u8, fg_b: u8,
|
||||
bg_r: u8, bg_g: u8, bg_b: u8,
|
||||
) {
|
||||
fill_rect(state, cell_x, cell_y, cell_w, cell_h, bg_r, bg_g, bg_b);
|
||||
|
||||
let glyph = match get_glyph(ch) {
|
||||
Some(g) => g,
|
||||
None => return,
|
||||
};
|
||||
|
||||
if cell_w < 3 || cell_h < 3 {
|
||||
return;
|
||||
}
|
||||
|
||||
let scale_h = (cell_w.saturating_sub(2)) / font::FONT_5X7_W;
|
||||
let scale_v = (cell_h.saturating_sub(2)) / font::FONT_5X7_H;
|
||||
let scale = scale_h.min(scale_v).min(8);
|
||||
|
||||
if scale == 0 {
|
||||
return;
|
||||
}
|
||||
|
||||
let glw = font::FONT_5X7_W * scale;
|
||||
let glh = font::FONT_5X7_H * scale;
|
||||
let off_x = (cell_w - glw) / 2;
|
||||
let off_y = (cell_h - glh) / 2;
|
||||
|
||||
for row in 0..font::FONT_5X7_H {
|
||||
for col in 0..font::FONT_5X7_W {
|
||||
if glyph[row] & (1 << (4 - col)) != 0 {
|
||||
fill_rect(
|
||||
state,
|
||||
cell_x + off_x + col * scale,
|
||||
cell_y + off_y + row * scale,
|
||||
scale, scale,
|
||||
fg_r, fg_g, fg_b,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn clear_screen(bg: Color) {
|
||||
let (r, g, b) = color_to_rgb(bg);
|
||||
with_fb(|state| {
|
||||
fill_rect(state, 0, 0, state.width, state.height, r, g, b);
|
||||
});
|
||||
}
|
||||
|
||||
pub fn write_char(row: usize, col: usize, ch: u8, color: ColorCode) {
|
||||
if row >= HEIGHT || col >= WIDTH {
|
||||
return;
|
||||
}
|
||||
let (fg_r, fg_g, fg_b) = color_to_rgb(color.fg);
|
||||
let (bg_r, bg_g, bg_b) = color_to_rgb(color.bg);
|
||||
with_fb(|state| {
|
||||
let cell_x = state.x_off + col * state.cell_w;
|
||||
let cell_y = state.y_off + row * state.cell_h;
|
||||
draw_glyph_at(
|
||||
state,
|
||||
cell_x, cell_y, state.cell_w, state.cell_h,
|
||||
ch,
|
||||
fg_r, fg_g, fg_b,
|
||||
bg_r, bg_g, bg_b,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
pub fn write_str(row: usize, col: usize, s: &str, color: ColorCode) {
|
||||
for (i, &ch) in s.as_bytes().iter().enumerate() {
|
||||
if col + i < WIDTH {
|
||||
write_char(row, col + i, ch, color);
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user