Untitled

 avatar
unknown
plain_text
a year ago
7.0 kB
16
Indexable
use rand::seq::SliceRandom;
use rand::Rng;
use std::io::{self, Write};

const N: usize = 4;

#[derive(Clone)]
struct Game {
    board: [[u16; N]; N],
    score: u32,
}

impl Game {
    fn new() -> Self {
        let mut g = Self {
            board: [[0; N]; N],
            score: 0,
        };
        g.spawn_tile();
        g.spawn_tile();
        g
    }

    fn reset(&mut self) {
        self.board = [[0; N]; N];
        self.score = 0;
        self.spawn_tile();
        self.spawn_tile();
    }

    fn spawn_tile(&mut self) -> bool {
        let mut empties: Vec<(usize, usize)> = Vec::new();
        for r in 0..N {
            for c in 0..N {
                if self.board[r][c] == 0 {
                    empties.push((r, c));
                }
            }
        }
        if empties.is_empty() {
            return false;
        }
        let mut rng = rand::thread_rng();
        let &(r, c) = empties.choose(&mut rng).unwrap();
        // 90% chance of 2, 10% chance of 4
        self.board[r][c] = if rng.gen::<f32>() < 0.9 { 2 } else { 4 };
        true
    }

    fn render(&self) {
        // Clear screen (ANSI); safe to ignore if the terminal doesn't support it
        print!("\x1B[2J\x1B[H");
        println!("2048 (W/A/S/D to move, N=new, Q=quit)");
        println!("Score: {}", self.score);
        println!("+------+------+------+------+");
        for r in 0..N {
            for c in 0..N {
                let v = self.board[r][c];
                if v == 0 {
                    print!("|{:^6}", ".");
                } else {
                    print!("|{:^6}", v);
                }
            }
            println!("|");
            println!("+------+------+------+------+");
        }
        io::stdout().flush().ok();
    }

    fn can_move(&self) -> bool {
        // If any zero exists, can move
        for r in 0..N {
            for c in 0..N {
                if self.board[r][c] == 0 {
                    return true;
                }
            }
        }
        // Otherwise, if any adjacent equal tiles exist, can move
        for r in 0..N {
            for c in 0..N {
                if r + 1 < N && self.board[r][c] == self.board[r + 1][c] {
                    return true;
                }
                if c + 1 < N && self.board[r][c] == self.board[r][c + 1] {
                    return true;
                }
            }
        }
        false
    }

    fn move_left(&mut self) -> bool {
        let mut moved_any = false;
        for r in 0..N {
            let line = self.board[r];
            let (new_line, gained, changed) = slide_and_merge_line(line);
            if changed {
                self.board[r] = new_line;
                moved_any = true;
                self.score = self.score.saturating_add(gained);
            }
        }
        moved_any
    }

    fn move_right(&mut self) -> bool {
        let mut moved_any = false;
        for r in 0..N {
            let mut line = self.board[r];
            line.reverse();
            let (mut new_line, gained, changed) = slide_and_merge_line(line);
            new_line.reverse();
            if changed {
                self.board[r] = new_line;
                moved_any = true;
                self.score = self.score.saturating_add(gained);
            }
        }
        moved_any
    }

    fn move_up(&mut self) -> bool {
        let mut moved_any = false;
        for c in 0..N {
            let mut line = [0u16; N];
            for r in 0..N {
                line[r] = self.board[r][c];
            }
            let (new_line, gained, changed) = slide_and_merge_line(line);
            if changed {
                for r in 0..N {
                    self.board[r][c] = new_line[r];
                }
                moved_any = true;
                self.score = self.score.saturating_add(gained);
            }
        }
        moved_any
    }

    fn move_down(&mut self) -> bool {
        let mut moved_any = false;
        for c in 0..N {
            let mut line = [0u16; N];
            for r in 0..N {
                line[r] = self.board[r][c];
            }
            line.reverse();
            let (mut new_line, gained, changed) = slide_and_merge_line(line);
            new_line.reverse();
            if changed {
                for r in 0..N {
                    self.board[r][c] = new_line[r];
                }
                moved_any = true;
                self.score = self.score.saturating_add(gained);
            }
        }
        moved_any
    }
}

/// Slide non-zeros to the left, merge equal neighbors once, then slide again.
/// Returns: (new_line, score_gained_from_merges, changed_flag)
fn slide_and_merge_line(line: [u16; N]) -> ([u16; N], u32, bool) {
    // Compress: remove zeros
    let mut non_zero: Vec<u16> = line.iter().copied().filter(|&x| x != 0).collect();

    // Merge once per tile from left to right
    let mut merged: Vec<u16> = Vec::with_capacity(N);
    let mut score_gain: u32 = 0;
    let mut skip = false;

    for i in 0..non_zero.len() {
        if skip {
            skip = false;
            continue;
        }
        if i + 1 < non_zero.len() && non_zero[i] == non_zero[i + 1] {
            let v = non_zero[i] * 2;
            merged.push(v);
            score_gain = score_gain.saturating_add(v as u32);
            skip = true;
        } else {
            merged.push(non_zero[i]);
        }
    }

    // Pad with zeros to the right
    while merged.len() < N {
        merged.push(0);
    }

    let mut new_line = [0u16; N];
    for i in 0..N {
        new_line[i] = merged[i];
    }
    let changed = new_line != line;
    (new_line, score_gain, changed)
}

fn read_input() -> Option<char> {
    print!("Move (W/A/S/D, N=new, Q=quit): ");
    io::stdout().flush().ok()?;
    let mut s = String::new();
    io::stdin().read_line(&mut s).ok()?;
    s.chars()
        .find(|ch| !ch.is_whitespace())
        .map(|ch| ch.to_ascii_lowercase())
}

fn main() {
    let mut game = Game::new();

    loop {
        game.render();
        if !game.can_move() {
            println!("Game Over! Final score: {}", game.score);
            match read_input() {
                Some('n') => {
                    game.reset();
                    continue;
                }
                Some('q') => break,
                _ => continue,
            }
        }

        let Some(ch) = read_input() else { continue };
        let moved = match ch {
            'w' => game.move_up(),
            'a' => game.move_left(),
            's' => game.move_down(),
            'd' => game.move_right(),
            'n' => {
                game.reset();
                continue;
            }
            'q' => break,
            _ => {
                // ignore invalid keys
                false
            }
        };

        if moved {
            // Only spawn a tile if the board actually changed
            game.spawn_tile();
        }
    }
}
Editor is loading...
Leave a Comment