Add game 2048

This commit is contained in:
2025-11-28 13:04:03 +01:00
parent da6e1ea7b0
commit 6190053330
11 changed files with 393 additions and 77 deletions

View File

@@ -1,6 +1,6 @@
[workspace]
resolver = "3"
members = ["fpga-lib", "fpga-lib-macros", "hello-rust"]
members = ["fpga-lib", "fpga-lib-macros", "hello-rust", "game-2048"]
[profile.release]
panic = "abort"

View File

@@ -1,28 +1,21 @@
pub struct Buttons {}
impl Buttons
{
pub unsafe fn get_button(button_id: u8) -> bool
{
unsafe { *LED_IP_ADDRESS >> (button_id) & 0b1 == 0b1 }
impl Buttons {
pub unsafe fn get_button(button_id: u8) -> bool {
unsafe { ((*LED_IP_ADDRESS) >> button_id) & 0b1 == 0b1 }
}
}
pub struct Leds {}
impl Leds
{
pub unsafe fn set_led(led_id: u8, state: bool)
{
if state
{
unsafe { *LED_IP_ADDRESS |= (state as u32) << (led_id + 4) }
}
else
{
unsafe { *LED_IP_ADDRESS &= !((state as u32) << (led_id + 4)) }
impl Leds {
pub unsafe fn set_led(led_id: u8, state: bool) {
if state {
unsafe { *LED_IP_ADDRESS |= (state as u64) << (led_id + 4) }
} else {
unsafe { *LED_IP_ADDRESS &= !((state as u64) << (led_id + 4)) }
}
}
}
pub const LED_IP_ADDRESS: *mut u32 = 0x30000000 as *mut u32;
pub const LED_IP_ADDRESS: *mut u64 = 0x30000000 as *mut u64;

View File

@@ -8,10 +8,11 @@ pub mod vga;
macro_rules! panic_handler {
() => {
#[panic_handler]
fn panic(_info: &core::panic::PanicInfo) -> !
{
loop
{}
fn panic(_info: &core::panic::PanicInfo) -> ! {
unsafe {
$crate::vga::VGA::draw_string(0, 0, "PANIC !", $crate::vga::WHITE);
}
loop {}
}
};
}

View File

@@ -1,3 +1,5 @@
use core::ptr::write_bytes;
use fpga_lib_macros::include_font_plate;
pub const VGA_ADDRESS: *mut Color = 0x80000000 as *mut Color;
@@ -8,10 +10,8 @@ pub const HEIGHT: usize = 240;
#[derive(Clone, Copy)]
pub struct Color(pub(crate) u8);
impl Color
{
pub const fn from_rgb(r: u8, g: u8, b: u8) -> Self
{
impl Color {
pub const fn from_rgb(r: u8, g: u8, b: u8) -> Self {
Self(((r >> 6) << 6) | ((g >> 5) << 3) | (b >> 5))
}
}
@@ -26,12 +26,10 @@ pub const BLUE: Color = Color::from_rgb(0, 0, 255);
pub struct VGA {}
impl VGA
{
impl VGA {
/// # Safety
/// `x` must be less than `WIDTH` and `y` must be less than `HEIGHT`
pub unsafe fn write_pixel_unsafe(x: u16, y: u16, color: Color)
{
pub unsafe fn write_pixel_unsafe(x: u16, y: u16, color: Color) {
let pixel_index = x as usize + y as usize * WIDTH;
let word_index = pixel_index / 4; // Get index of the word in which the pixel is
let byte_index = pixel_index % 4; // Get index of the byte within the word for the pixel
@@ -41,14 +39,10 @@ impl VGA
/// # Safety
/// `x` must be less than `WIDTH` and `y` must be less than `HEIGHT`
pub unsafe fn draw_char(x: u16, y: u16, c: char, color: Color)
{
let c = if (c as u8 > b'~') || ((c as u8) < b' ')
{
pub unsafe fn draw_char(x: u16, y: u16, c: char, color: Color) {
let c = if (c as u8 > b'~') || ((c as u8) < b' ') {
b'/' - b' '
}
else
{
} else {
c as u8 - b' '
};
@@ -56,10 +50,8 @@ impl VGA
let char_x = (c as usize % 32) * FONT_WIDTH;
let char_y = (c as usize / 32) * FONT_HEIGHT;
for i in 0..(FONT_WIDTH as u16)
{
for j in 0..(FONT_HEIGHT as u16)
{
for i in 0..(FONT_WIDTH as u16) {
for j in 0..(FONT_HEIGHT as u16) {
let xx = x + i;
let yy = y + j;
@@ -73,14 +65,10 @@ impl VGA
}
}
pub unsafe fn draw_char_bg(x: u16, y: u16, c: char, color: Color, bg_color: Color)
{
let c = if (c as u8 > b'~') || ((c as u8) < b' ')
{
pub unsafe fn draw_char_bg(x: u16, y: u16, c: char, color: Color, bg_color: Color) {
let c = if (c as u8 > b'~') || ((c as u8) < b' ') {
b'/' - b' '
}
else
{
} else {
c as u8 - b' '
};
@@ -88,21 +76,15 @@ impl VGA
let char_x = (c as usize % 32) * FONT_WIDTH;
let char_y = (c as usize / 32) * FONT_HEIGHT;
for i in 0..(FONT_WIDTH as u16)
{
for j in 0..(FONT_HEIGHT as u16)
{
for i in 0..(FONT_WIDTH as u16) {
for j in 0..(FONT_HEIGHT as u16) {
let xx = x + i;
let yy = y + j;
if xx < (WIDTH as u16) && yy < (HEIGHT as u16)
{
if unsafe { Self::font_plate_index(char_x as u16 + i, char_y as u16 + j) }
{
if xx < (WIDTH as u16) && yy < (HEIGHT as u16) {
if unsafe { Self::font_plate_index(char_x as u16 + i, char_y as u16 + j) } {
unsafe { Self::write_pixel_unsafe(xx, yy, color) }
}
else
{
} else {
unsafe { Self::write_pixel_unsafe(xx, yy, bg_color) }
}
}
@@ -112,18 +94,18 @@ impl VGA
/// # Safety
/// The text must have a length that can fit within a `u16`
pub unsafe fn draw_string(x: u16, y: u16, str: &str, color: Color)
{
pub unsafe fn draw_string(x: u16, y: u16, str: &str, color: Color) {
str.chars().enumerate().for_each(|(i, c)| unsafe {
Self::draw_char(x + i as u16 * (FONT_WIDTH as u16), y, c as char, color)
});
}
pub fn clear_screen(color: Color) {
unsafe { write_bytes(VGA_ADDRESS, color.0, WIDTH * HEIGHT) };
}
fn digit_count(mut val: u64) -> u64
{
fn digit_count(mut val: u64) -> u64 {
let mut i = 0;
while val != 0
{
while val != 0 {
val /= 10;
i += 1;
}
@@ -131,17 +113,14 @@ impl VGA
i
}
pub unsafe fn draw_u64(x: u16, y: u16, mut val: u64, color: Color, bg_color: Color)
{
if val == 0
{
pub unsafe fn draw_u64(x: u16, y: u16, mut val: u64, color: Color, bg_color: Color) {
if val == 0 {
unsafe { Self::draw_char_bg(x, y, '0', color, bg_color) }
return;
}
let digit_count = Self::digit_count(val) - 1;
let mut i = digit_count as u16;
while val != 0
{
while val != 0 {
let digit = val % 10;
val /= 10;
@@ -160,8 +139,7 @@ impl VGA
}
}
unsafe fn font_plate_index(x: u16, y: u16) -> bool
{
unsafe fn font_plate_index(x: u16, y: u16) -> bool {
let pixel_index = (y as usize) * FONTPLATE_WIDTH + (x as usize);
let byte_index = pixel_index / 8;
let bit_index = pixel_index % 8;

View File

@@ -0,0 +1 @@
/target

View File

@@ -0,0 +1,8 @@
[package]
name = "game-2048"
version = "0.1.0"
edition = "2024"
[dependencies]
fpga-lib = { path = "../fpga-lib" }
rand = {version = "0.9.2", default-features = false, features = ["small_rng"]}

View File

@@ -0,0 +1,3 @@
# 2048
A terminal version of the 2048 game.

View File

@@ -0,0 +1,332 @@
#![no_std]
#![no_main]
#![allow(incomplete_features)]
#![feature(generic_const_exprs)]
use core::{
iter::Cloned,
mem::MaybeUninit,
ops::{Index, IndexMut},
};
use fpga_lib::{
panic_handler,
vga::{Color, BLACK, FONT_HEIGHT, FONT_WIDTH, RED, WHITE},
};
use rand::{seq::IndexedRandom, Rng, SeedableRng};
const fn get_color(x: u64) -> Color {
match x {
2 => Color::from_rgb(238, 228, 222),
4 => Color::from_rgb(237, 224, 200),
8 => Color::from_rgb(242, 177, 120),
16 => Color::from_rgb(245, 149, 99),
32 => Color::from_rgb(246, 124, 95),
64 => Color::from_rgb(246, 94, 59),
128 => Color::from_rgb(237, 207, 114),
256 => Color::from_rgb(237, 204, 97),
512 => Color::from_rgb(237, 200, 80),
1024 => Color::from_rgb(237, 197, 63),
2048 => Color::from_rgb(237, 194, 46),
4096 => Color::from_rgb(238, 102, 106),
8192 => Color::from_rgb(238, 78, 90),
16384 => Color::from_rgb(241, 65, 65),
32768 => Color::from_rgb(114, 178, 212),
65536 => Color::from_rgb(88, 159, 226),
131072 => Color::from_rgb(23, 129, 204),
_ => Color::from_rgb(255, 255, 255),
}
}
pub struct NZip<T, const N: usize> {
iters: [T; N],
}
impl<T: Iterator, const N: usize> Iterator for NZip<T, N> {
type Item = [T::Item; N];
fn next(&mut self) -> Option<Self::Item> {
let mut res = MaybeUninit::<[T::Item; N]>::uninit();
for (cpt, iter) in self.iters.iter_mut().enumerate() {
unsafe { *(res.as_mut_ptr() as *mut T::Item).add(cpt) = iter.next()? };
}
Some(unsafe { res.assume_init() })
}
}
impl<T: DoubleEndedIterator, const N: usize> DoubleEndedIterator for NZip<T, N> {
fn next_back(&mut self) -> Option<Self::Item> {
let mut res = MaybeUninit::<[T::Item; N]>::uninit();
for (cpt, iter) in self.iters.iter_mut().enumerate() {
unsafe { *(res.as_mut_ptr() as *mut T::Item).add(cpt) = iter.next_back()? };
}
Some(unsafe { res.assume_init() })
}
}
#[derive(Clone)]
pub struct Game<const SIZE: usize>
where
[(); SIZE * SIZE]:,
{
grid: [u64; SIZE * SIZE],
rng: rand::rngs::SmallRng,
score: u64,
}
impl<const SIZE: usize> Index<(usize, usize)> for Game<SIZE>
where
[(); SIZE * SIZE]:,
{
type Output = u64;
fn index(&self, index: (usize, usize)) -> &Self::Output {
&self.grid[index.1 * SIZE + index.0]
}
}
impl<const SIZE: usize> IndexMut<(usize, usize)> for Game<SIZE>
where
[(); SIZE * SIZE]:,
{
fn index_mut(&mut self, index: (usize, usize)) -> &mut Self::Output {
&mut self.grid[index.1 * SIZE + index.0]
}
}
impl<const SIZE: usize> Game<SIZE>
where
[(); SIZE * SIZE]:,
{
pub fn new() -> Self {
Self {
grid: [0; SIZE * SIZE],
rng: rand::rngs::SmallRng::seed_from_u64(9543223934),
score: 0,
}
}
pub fn init() -> Self {
let mut g = Self::new();
g.add();
g.add();
g
}
pub fn reset(&mut self) {
*self = Self::new()
}
pub fn add(&mut self) {
let mut pos = [(0, 0); SIZE * SIZE];
let mut cpt = 0;
for y in 0..SIZE {
for x in 0..SIZE {
if self[(x, y)] == 0 {
pos[cpt] = (x, y);
cpt += 1;
}
}
}
let pos = *pos[0..cpt].choose(&mut self.rng).unwrap();
self[pos] = (self.rng.random_range(0..=1) + 1) * 2;
}
pub fn lost(&self) -> bool {
let mut copy = self.clone();
!(copy.left() || copy.down() || copy.right() || copy.up())
}
fn rotate(&mut self) {
let mut out = self.grid;
for i in 0..SIZE {
for j in 0..SIZE {
out[j * SIZE + (SIZE - i - 1)] = self.grid[i * SIZE + j];
}
}
self.grid = out;
}
fn rotate_back(&mut self) {
let mut out = self.grid;
for i in 0..SIZE {
for j in 0..SIZE {
out[(SIZE - j - 1) * SIZE + i] = self.grid[i * SIZE + j];
}
}
self.grid = out;
}
pub fn left(&mut self) -> bool {
let mut changed = false;
for y in 0..SIZE {
let mut index = 0;
let mut fixed = false;
let mut zero_found = false;
for x in 0..SIZE {
if self[(x, y)] == 0 {
zero_found = true;
continue;
} else if zero_found {
changed = true;
}
if index != 0 && self[(index - 1, y)] == (self[(x, y)]) && !fixed {
self[(index - 1, y)] = self[(x, y)] * 2;
self[(x, y)] = 0;
self.score += self[(index - 1, y)];
fixed = true;
changed = true;
} else {
self[(index, y)] = self[(x, y)];
if index != x {
self[(x, y)] = 0;
}
fixed = false;
index += 1;
}
}
}
changed
}
pub fn down(&mut self) -> bool {
self.rotate();
let res = self.left();
self.rotate_back();
res
}
pub fn right(&mut self) -> bool {
self.rotate();
self.rotate();
let res = self.left();
self.rotate_back();
self.rotate_back();
res
}
pub fn up(&mut self) -> bool {
self.rotate_back();
let res = self.left();
self.rotate();
res
}
pub fn run(&mut self) -> ! {
let mut buttons = [false; 4];
let funcs = [Self::left, Self::right, Self::up, Self::down];
loop {
let mut lost = false;
self.draw();
loop {
for direction in 0..4 {
let button =
unsafe { fpga_lib::interface::Buttons::get_button(direction as u8) };
if button && !buttons[direction] {
buttons[direction] = true;
if funcs[direction](self) {
self.add();
self.draw();
if self.lost() {
lost = true;
break;
}
}
} else if !button && buttons[direction] {
buttons[direction] = false
}
}
if lost {
unsafe {
fpga_lib::vga::VGA::draw_string(
OFFSET,
OFFSET + (SIZE as u16) * BLOCK_SIZE + FONT_HEIGHT as u16 + 10,
"Game Over !",
RED,
)
};
self.reset();
while !(0..4).any(|direction| unsafe {
fpga_lib::interface::Buttons::get_button(direction as u8)
}) {}
break;
}
}
}
}
}
impl<const SIZE: usize> Default for Game<SIZE>
where
[(); SIZE * SIZE]:,
{
fn default() -> Self {
Self::new()
}
}
impl<const SIZE: usize> Game<SIZE>
where
[(); SIZE * SIZE]:,
{
fn draw(&self) {
fpga_lib::vga::VGA::clear_screen(BLACK);
for i in 0..SIZE as u16 + 1 {
for x in 0..BLOCK_SIZE * SIZE as u16 {
unsafe {
fpga_lib::vga::VGA::write_pixel_unsafe(
OFFSET + x,
OFFSET + i * BLOCK_SIZE,
WHITE,
)
};
unsafe {
fpga_lib::vga::VGA::write_pixel_unsafe(
OFFSET + i * BLOCK_SIZE,
OFFSET + x,
WHITE,
)
};
}
}
for y in 0..SIZE {
for x in 0..SIZE {
if self[(x, y)] != 0 {
unsafe {
fpga_lib::vga::VGA::draw_u64(
OFFSET + x as u16 * BLOCK_SIZE + BLOCK_SIZE / 2
- (fpga_lib::vga::FONT_WIDTH as u16
* (self[(x, y)].ilog10() as u16 + 1))
/ 2,
OFFSET + y as u16 * BLOCK_SIZE + BLOCK_SIZE / 2
- fpga_lib::vga::FONT_HEIGHT as u16 / 2,
self[(x, y)],
get_color(self[(x, y)]),
BLACK,
)
};
}
}
}
unsafe {
fpga_lib::vga::VGA::draw_string(
OFFSET,
OFFSET + (SIZE as u16) * BLOCK_SIZE + 5,
"Score :",
WHITE,
);
fpga_lib::vga::VGA::draw_u64(
OFFSET + FONT_WIDTH as u16 * 8,
OFFSET + (SIZE as u16) * BLOCK_SIZE + 5,
self.score,
WHITE,
BLACK,
);
}
}
}
panic_handler! {}
#[unsafe(no_mangle)]
pub extern "C" fn main() -> ! {
let mut g = Game::<4>::init();
g.run()
}
const BLOCK_SIZE: u16 = 40;
const OFFSET: u16 = 20;

View File

@@ -20,7 +20,7 @@ set_property -dict { PACKAGE_PIN T16 IOSTANDARD LVCMOS33 } [get_ports { io_swi
set_property -dict { PACKAGE_PIN R18 IOSTANDARD LVCMOS33 } [get_ports { io_push[0] }]; #IO_L20N_T3_34 Sch=BTN0
set_property -dict { PACKAGE_PIN P16 IOSTANDARD LVCMOS33 } [get_ports { io_push[1] }]; #IO_L24N_T3_34 Sch=BTN1
set_property -dict { PACKAGE_PIN V16 IOSTANDARD LVCMOS33 } [get_ports { io_push[2] }]; #IO_L18P_T2_34 Sch=BTN2
set_property -dict { PACKAGE_PIN Y16 IOSTANDARD LVCMOS33 } [get_ports { reset }]; #IO_L7P_T1_34 Sch=BTN3
set_property -dict { PACKAGE_PIN Y16 IOSTANDARD LVCMOS33 } [get_ports { io_push[3] }]; #IO_L7P_T1_34 Sch=BTN3
# LEDs

View File

@@ -10,7 +10,7 @@ class LedIp extends Module {
val x31 = Input(UInt(64.W))
val switch = Input(UInt(4.W))
val led = Output(UInt(4.W))
val button = Input(UInt(3.W))
val button = Input(UInt(4.W))
val bus = new BusInterface
})
@@ -27,7 +27,7 @@ class LedIp extends Module {
when(io.bus.be.asUInt.orR) { // Écriture dans le registre des leds internes
dinReg := io.bus.wdata
}.otherwise { // Lecture des boutons poussoirs, pas de débounce car lus par le soft
btnReg := Fill(29, 0.U) ## io.button
btnReg := Fill(28, 0.U) ## io.button
}
}

View File

@@ -7,7 +7,7 @@ class ZzTop(file: String = "", sim: Boolean = true) extends Module {
// Interface debug minimale
val switch = Input(UInt(4.W))
val led = Output(UInt(4.W))
val push = Input(UInt(3.W))
val push = Input(UInt(4.W))
// Debug simu pour ne pas s'embeter avec le switch dans le testbench
val x31 = if (sim) Some(Output(UInt(64.W))) else None
val valid_x31 = if (sim) Some(Output(Bool())) else None