merosity 0.1.0

(wip) competitive stacker game
// merosity, online stacker game
//
// Copyright (c) 2023 rini
// SPDX-License-Identifier: Apache-2.0

use std::sync::Arc;

use bevy::ecs::prelude::*;
use fastrand::Rng;

pub use board::Board;
pub use minos::{Mino, Tile};
pub use piece::{Piece, Rotation, Turn};

mod board;
mod minos;
mod piece;

pub enum Generator {
    Bag,
}

impl Generator {
    pub fn generate(&self, queue: &mut Vec<Mino>, rng: &mut Rng) {
        use Mino::*;

        match self {
            Self::Bag => {
                let mut bag = [I, L, J, S, Z, T, O];
                rng.shuffle(&mut bag);
                queue.extend_from_slice(&bag);
            }
        }
    }
}

#[derive(Component)]
pub struct Game {
    pub lockout: bool,
    engine: Arc<Engine>,
    shape: (usize, usize),
    pub board: Board<Tile>,
    pub piece: Piece,
    pub queue: Vec<Mino>,
    rng: Rng,
}

#[derive(Resource)]
pub struct Engine {
    pub shape: (usize, usize),
    pub seed: u64,
    pub rand: Generator,
}

impl Default for Engine {
    fn default() -> Self {
        let mut buf = [0; 8];
        getrandom::getrandom(&mut buf).unwrap();

        Self {
            shape: (10, 40),
            seed: u64::from_be_bytes(buf),
            rand: Generator::Bag,
        }
    }
}

impl Game {
    pub fn new(engine: Arc<Engine>) -> Self {
        let shape = engine.shape;
        let mut queue = Vec::new();
        let mut rng = Rng::with_seed(engine.seed);
        engine.rand.generate(&mut queue, &mut rng);

        Game {
            lockout: false,
            engine,
            shape,
            board: Board::filled(shape, Tile::Empty),
            piece: Piece::new(shape, queue.pop().unwrap()),
            queue,
            rng,
        }
    }

    pub fn translate(&mut self, dx: isize, dy: isize) -> bool {
        let mut collided = false;
        for (x, y) in &mut self.piece.parts {
            *x += dx;
            *y += dy;

            if !(0..self.shape.0 as isize).contains(x)
                || !(0..self.shape.1 as isize).contains(y)
                || self.board[*y as usize][*x as usize] != Tile::Empty
            {
                collided = true;
            }
        }

        if collided {
            for (x, y) in &mut self.piece.parts {
                *x -= dx;
                *y -= dy;
            }
        } else {
            self.piece.pivot.0 += dx;
            self.piece.pivot.1 += dy;
        }

        collided
    }

    pub fn hard_drop(&mut self) {
        while !self.translate(0, -1) {}

        self.lock_piece();
    }

    fn lock_piece(&mut self) {
        for (x, y) in &self.piece.parts {
            self.board[*y as usize][*x as usize] = Tile::Mino(self.piece.kind.clone());
        }

        let mut i = 0;
        while i < self.shape.1 {
            if self.board[i].iter().all(|t| t != &Tile::Empty) {
                for j in i..self.shape.1 - 1 {
                    let mut next = self.board[j + 1].to_vec();
                    self.board[j].swap_with_slice(&mut next);
                }

                self.board[self.shape.1 - 1].fill(Tile::Empty);
            } else {
                i += 1;
            }
        }

        let piece = self.next_piece();
        for (x, y) in &piece.parts {
            if self.board[*y as usize][*x as usize] != Tile::Empty {
                self.lockout = true;
                return;
            }
        }

        self.piece = piece;
    }

    fn next_piece(&mut self) -> Piece {
        while self.queue.len() < 4 {
            self.engine.rand.generate(&mut self.queue, &mut self.rng);
        }

        Piece::new(self.shape, self.queue.pop().unwrap())
    }

    pub fn rotate(&mut self, turn: Turn) -> bool {
        self.rotate_direct(turn);

        let dir = Rotation::from(turn as i8 + self.piece.dir as i8);

        for ((ax, ay), (bx, by)) in kicks(&self.piece.kind, self.piece.dir)
            .iter()
            .zip(kicks(&self.piece.kind, dir))
        {
            if !self.translate(ax - bx, ay - by) {
                self.piece.dir = dir;
                return false;
            }
        }

        // revert if nothing worked
        self.rotate_direct(match turn {
            Turn::Cw => Turn::Ccw,
            Turn::Ccw => Turn::Cw,
        });

        true
    }

    fn rotate_direct(&mut self, turn: Turn) {
        let (px, py) = self.piece.pivot;
        for (x, y) in &mut self.piece.parts {
            // rotate by transposing and flipping axis around pivot
            std::mem::swap(x, y);
            match turn {
                Turn::Cw => {
                    *x = *x - py + px;
                    *y = px - *y + py;
                }
                Turn::Ccw => {
                    *x = py - *x + px;
                    *y = *y - px + py;
                }
            }
        }
    }

    pub fn ghost(&self) -> isize {
        let mut i = 1;

        loop {
            for (x, y) in &self.piece.parts {
                if *y - i < 0 || self.board[(*y - i) as usize][*x as usize] != Tile::Empty {
                    return i - 1;
                }
            }

            i += 1;
        }
    }
}

#[rustfmt::skip]
fn kicks(mino: &Mino, dir: Rotation) -> [(isize, isize); 5] {
    match mino {
        Mino::O => match dir {
            Rotation::Spawn => [( 0,  0), ( 0,  0), ( 0,  0), ( 0,  0), ( 0,  0)],
            Rotation::Right => [( 0, -1), ( 0,  0), ( 0,  0), ( 0,  0), ( 0,  0)],
            Rotation::Down  => [(-1, -1), ( 0,  0), ( 0,  0), ( 0,  0), ( 0,  0)],
            Rotation::Left  => [(-1,  0), ( 0,  0), ( 0,  0), ( 0,  0), ( 0,  0)],
        },
        Mino::I => match dir {
            Rotation::Spawn => [( 0,  0), (-1,  0), ( 2,  0), (-1,  0), ( 2,  0)],
            Rotation::Right => [(-1,  0), ( 0,  0), ( 0,  0), ( 0,  1), ( 0, -2)],
            Rotation::Down  => [(-1,  1), ( 1,  1), (-2,  1), ( 1,  0), (-2,  0)],
            Rotation::Left  => [( 0,  1), ( 0,  1), ( 0,  1), ( 0, -1), ( 0,  2)],
        },
        _ => match dir {
            Rotation::Spawn => [( 0,  0), ( 0,  0), ( 0,  0), ( 0,  0), ( 0,  0)],
            Rotation::Right => [( 0,  0), ( 1,  0), ( 1, -1), ( 0,  2), ( 1,  2)],
            Rotation::Down  => [( 0,  0), ( 0,  0), ( 0,  0), ( 0,  0), ( 0,  0)],
            Rotation::Left  => [( 0,  0), (-1,  0), (-1, -1), ( 0,  2), (-1,  2)],
        },
    }
}