Initial commit

This commit is contained in:
missing 2022-04-29 14:05:01 -05:00
parent 1c60643479
commit fc7d4e69d6
5 changed files with 563 additions and 0 deletions

10
Cargo.toml Normal file
View file

@ -0,0 +1,10 @@
[package]
name = "pacman"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
pancurses = "0.17.0"
rand = "0.8.5"

31
src/board.txt Normal file
View file

@ -0,0 +1,31 @@
█▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀██▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀█
█ ██ █
█ ▄▄▄▄▄▄ ▄▄▄▄▄▄▄▄ ██ ▄▄▄▄▄▄▄▄ ▄▄▄▄▄▄ █
█ █ █ █ █ ██ █ █ █ █ █
█ ▀▀▀▀▀▀ ▀▀▀▀▀▀▀▀ ▀▀ ▀▀▀▀▀▀▀▀ ▀▀▀▀▀▀ █
█ █
█ ▄▄▄▄▄▄ ▄▄ ▄▄▄▄▄▄▄▄▄▄▄▄▄▄ ▄▄ ▄▄▄▄▄▄ █
█ ▀▀▀▀▀▀ ██ ▀▀▀▀▀▀██▀▀▀▀▀▀ ██ ▀▀▀▀▀▀ █
█ ██ ██ ██ █
█▄▄▄▄▄▄▄▄▄▄ ██▄▄▄▄▄▄ ██ ▄▄▄▄▄▄██ ▄▄▄▄▄▄▄▄▄▄█
█ ██▀▀▀▀▀▀ ▀▀ ▀▀▀▀▀▀██ █
█ ██ ██ █
█ ██ ▄▄▄▄▄____▄▄▄▄▄ ██ █
▀▀▀▀▀▀▀▀▀▀▀ ▀▀ █ █ ▀▀ ▀▀▀▀▀▀▀▀▀▀▀
█ █
▄▄▄▄▄▄▄▄▄▄▄ ▄▄ █ █ ▄▄ ▄▄▄▄▄▄▄▄▄▄▄
█ ██ ▀▀▀▀▀▀▀▀▀▀▀▀▀▀ ██ █
█ ██ ██ █
█ ██ ▄▄▄▄▄▄▄▄▄▄▄▄▄▄ ██ █
█▀▀▀▀▀▀▀▀▀▀ ▀▀ ▀▀▀▀▀▀██▀▀▀▀▀▀ ▀▀ ▀▀▀▀▀▀▀▀▀▀█
█ ██ █
█ ▄▄▄▄▄▄ ▄▄▄▄▄▄▄▄ ██ ▄▄▄▄▄▄▄▄ ▄▄▄▄▄▄ █
█ ▀▀▀▀██ ▀▀▀▀▀▀▀▀ ▀▀ ▀▀▀▀▀▀▀▀ ██▀▀▀▀ █
█ ██ ██ █
█▄▄▄▄ ██ ▄▄ ▄▄▄▄▄▄▄▄▄▄▄▄▄▄ ▄▄ ██ ▄▄▄▄█
█▀▀▀▀ ▀▀ ██ ▀▀▀▀▀▀██▀▀▀▀▀▀ ██ ▀▀ ▀▀▀▀█
█ ██ ██ ██ █
█ ▄▄▄▄▄▄▄▄▄▄██▄▄▄▄▄▄ ██ ▄▄▄▄▄▄██▄▄▄▄▄▄▄▄▄▄ █
█ ▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ ▀▀ ▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ █
█ █
█▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄█

64
src/data.rs Normal file
View file

@ -0,0 +1,64 @@
use std::ops::{Index, IndexMut};
use pacman::Vec2;
const TT: bool = true;
#[allow(non_upper_case_globals)]
const o: bool = false;
pub const BOARD_WIDTH: usize = 28;
pub const BOARD_HEIGHT: usize = 31;
pub struct Board(pub [[bool; BOARD_WIDTH]; BOARD_HEIGHT]);
impl Index<Vec2> for Board {
type Output = bool;
fn index(&self, index: Vec2) -> &Self::Output {
&self.0
[index.y.rem_euclid(BOARD_HEIGHT as i32) as usize]
[index.x.rem_euclid(BOARD_WIDTH as i32) as usize]
}
}
impl IndexMut<Vec2> for Board {
fn index_mut(&mut self, index: Vec2) -> &mut Self::Output {
&mut self.0
[index.y.rem_euclid(BOARD_HEIGHT as i32) as usize]
[index.x.rem_euclid(BOARD_WIDTH as i32) as usize]
}
}
pub const BOARD: Board = Board([
[TT,TT,TT,TT,TT,TT,TT,TT,TT,TT,TT,TT,TT,TT,TT,TT,TT,TT,TT,TT,TT,TT,TT,TT,TT,TT,TT,TT],
[TT, o, o, o, o, o, o, o, o, o, o, o, o,TT,TT, o, o, o, o, o, o, o, o, o, o, o, o,TT],
[TT, o,TT,TT,TT,TT, o,TT,TT,TT,TT,TT, o,TT,TT, o,TT,TT,TT,TT,TT, o,TT,TT,TT,TT, o,TT],
[TT, o,TT,TT,TT,TT, o,TT,TT,TT,TT,TT, o,TT,TT, o,TT,TT,TT,TT,TT, o,TT,TT,TT,TT, o,TT],
[TT, o,TT,TT,TT,TT, o,TT,TT,TT,TT,TT, o,TT,TT, o,TT,TT,TT,TT,TT, o,TT,TT,TT,TT, o,TT],
[TT, o, o, o, o, o, o, o, o, o, o, o, o, o, o, o, o, o, o, o, o, o, o, o, o, o, o,TT],
[TT, o,TT,TT,TT,TT, o,TT,TT, o,TT,TT,TT,TT,TT,TT,TT,TT, o,TT,TT, o,TT,TT,TT,TT, o,TT],
[TT, o,TT,TT,TT,TT, o,TT,TT, o,TT,TT,TT,TT,TT,TT,TT,TT, o,TT,TT, o,TT,TT,TT,TT, o,TT],
[TT, o, o, o, o, o, o,TT,TT, o, o, o, o,TT,TT, o, o, o, o,TT,TT, o, o, o, o, o, o,TT],
[TT,TT,TT,TT,TT,TT, o,TT,TT,TT,TT,TT, o,TT,TT, o,TT,TT,TT,TT,TT, o,TT,TT,TT,TT,TT,TT],
[TT,TT,TT,TT,TT,TT, o,TT,TT,TT,TT,TT, o,TT,TT, o,TT,TT,TT,TT,TT, o,TT,TT,TT,TT,TT,TT],
[TT,TT,TT,TT,TT,TT, o,TT,TT, o, o, o, o, o, o, o, o, o, o,TT,TT, o,TT,TT,TT,TT,TT,TT],
[TT,TT,TT,TT,TT,TT, o,TT,TT, o,TT,TT,TT,TT,TT,TT,TT,TT, o,TT,TT, o,TT,TT,TT,TT,TT,TT],
[TT,TT,TT,TT,TT,TT, o,TT,TT, o,TT,TT,TT,TT,TT,TT,TT,TT, o,TT,TT, o,TT,TT,TT,TT,TT,TT],
[ o, o, o, o, o, o, o, o, o, o,TT,TT,TT,TT,TT,TT,TT,TT, o, o, o, o, o, o, o, o, o, o],
[TT,TT,TT,TT,TT,TT, o,TT,TT, o,TT,TT,TT,TT,TT,TT,TT,TT, o,TT,TT, o,TT,TT,TT,TT,TT,TT],
[TT,TT,TT,TT,TT,TT, o,TT,TT, o,TT,TT,TT,TT,TT,TT,TT,TT, o,TT,TT, o,TT,TT,TT,TT,TT,TT],
[TT,TT,TT,TT,TT,TT, o,TT,TT, o, o, o, o, o, o, o, o, o, o,TT,TT, o,TT,TT,TT,TT,TT,TT],
[TT,TT,TT,TT,TT,TT, o,TT,TT, o,TT,TT,TT,TT,TT,TT,TT,TT, o,TT,TT, o,TT,TT,TT,TT,TT,TT],
[TT,TT,TT,TT,TT,TT, o,TT,TT, o,TT,TT,TT,TT,TT,TT,TT,TT, o,TT,TT, o,TT,TT,TT,TT,TT,TT],
[TT, o, o, o, o, o, o, o, o, o, o, o, o,TT,TT, o, o, o, o, o, o, o, o, o, o, o, o,TT],
[TT, o,TT,TT,TT,TT, o,TT,TT,TT,TT,TT, o,TT,TT, o,TT,TT,TT,TT,TT, o,TT,TT,TT,TT, o,TT],
[TT, o,TT,TT,TT,TT, o,TT,TT,TT,TT,TT, o,TT,TT, o,TT,TT,TT,TT,TT, o,TT,TT,TT,TT, o,TT],
[TT, o, o, o,TT,TT, o, o, o, o, o, o, o, o, o, o, o, o, o, o, o, o,TT,TT, o, o, o,TT],
[TT,TT,TT, o,TT,TT, o,TT,TT, o,TT,TT,TT,TT,TT,TT,TT,TT, o,TT,TT, o,TT,TT, o,TT,TT,TT],
[TT,TT,TT, o,TT,TT, o,TT,TT, o,TT,TT,TT,TT,TT,TT,TT,TT, o,TT,TT, o,TT,TT, o,TT,TT,TT],
[TT, o, o, o, o, o, o,TT,TT, o, o, o, o,TT,TT, o, o, o, o,TT,TT, o, o, o, o, o, o,TT],
[TT, o,TT,TT,TT,TT,TT,TT,TT,TT,TT,TT, o,TT,TT, o,TT,TT,TT,TT,TT,TT,TT,TT,TT,TT, o,TT],
[TT, o,TT,TT,TT,TT,TT,TT,TT,TT,TT,TT, o,TT,TT, o,TT,TT,TT,TT,TT,TT,TT,TT,TT,TT, o,TT],
[TT, o, o, o, o, o, o, o, o, o, o, o, o, o, o, o, o, o, o, o, o, o, o, o, o, o, o,TT],
[TT,TT,TT,TT,TT,TT,TT,TT,TT,TT,TT,TT,TT,TT,TT,TT,TT,TT,TT,TT,TT,TT,TT,TT,TT,TT,TT,TT],
]);

189
src/lib.rs Normal file
View file

@ -0,0 +1,189 @@
use std::ops::{Add, Sub, Mul, Div, AddAssign, SubAssign, MulAssign, DivAssign};
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum Direction {
North,
South,
East,
West
}
impl Direction {
pub fn flipped(self) -> Self {
use Direction::*;
match self {
North => South,
South => North,
East => West,
West => East,
}
}
}
impl From<Direction> for Vec2 {
fn from(v: Direction) -> Self {
use Direction::*;
match v {
North => (0, -1).into(),
South => (0, 1).into(),
East => (1, 0).into(),
West => (-1, 0).into(),
}
}
}
impl Mul<i32> for Direction {
type Output = Vec2;
fn mul(self, rhs: i32) -> Self::Output {
Vec2::from(self) * rhs
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash)]
pub struct Vec2 {
pub x: i32,
pub y: i32
}
impl Vec2 {
pub fn sq_magnitude(self) -> i32 {
self.x * self.x + self.y * self.y
}
pub fn magnitude(self) -> f32 {
(self.sq_magnitude() as f32).sqrt()
}
pub fn sq_dist(self, rhs: Self) -> i32 {
(self - rhs).sq_magnitude()
}
pub fn dist(self, rhs: Self) -> f32 {
(self - rhs).magnitude()
}
}
impl From<(i32, i32)> for Vec2 {
fn from(v: (i32, i32)) -> Self {
Self { x: v.0, y: v.1 }
}
}
impl From<Vec2> for (i32, i32) {
fn from(v: Vec2) -> Self {
(v.x, v.y)
}
}
impl Add for Vec2 {
type Output = Self;
fn add(self, rhs: Self) -> Self::Output {
Self { x: self.x + rhs.x, y: self.y + rhs.y }
}
}
impl Add<(i32, i32)> for Vec2 {
type Output = Self;
fn add(self, rhs: (i32, i32)) -> Self::Output {
self + Self::from(rhs)
}
}
impl Add<Direction> for Vec2 {
type Output = Self;
fn add(self, rhs: Direction) -> Self::Output {
self + Self::from(rhs)
}
}
impl Sub for Vec2 {
type Output = Self;
fn sub(self, rhs: Self) -> Self::Output {
Self { x: self.x - rhs.x, y: self.y - rhs.y }
}
}
impl Sub<(i32, i32)> for Vec2 {
type Output = Self;
fn sub(self, rhs: (i32, i32)) -> Self::Output {
self - Self::from(rhs)
}
}
impl Sub<Direction> for Vec2 {
type Output = Self;
fn sub(self, rhs: Direction) -> Self::Output {
self - Self::from(rhs)
}
}
impl Mul<i32> for Vec2 {
type Output = Self;
fn mul(self, rhs: i32) -> Self::Output {
Self { x: self.x * rhs, y: self.y * rhs }
}
}
impl Div<i32> for Vec2 {
type Output = Self;
fn div(self, rhs: i32) -> Self::Output {
Self { x: self.x / rhs, y: self.y / rhs }
}
}
impl AddAssign for Vec2 {
fn add_assign(&mut self, rhs: Self) {
*self = *self + rhs
}
}
impl AddAssign<(i32, i32)> for Vec2 {
fn add_assign(&mut self, rhs: (i32, i32)) {
*self = *self + rhs
}
}
impl AddAssign<Direction> for Vec2 {
fn add_assign(&mut self, rhs: Direction) {
*self = *self + rhs
}
}
impl SubAssign for Vec2 {
fn sub_assign(&mut self, rhs: Self) {
*self = *self - rhs
}
}
impl SubAssign<(i32, i32)> for Vec2 {
fn sub_assign(&mut self, rhs: (i32, i32)) {
*self = *self - rhs
}
}
impl SubAssign<Direction> for Vec2 {
fn sub_assign(&mut self, rhs: Direction) {
*self = *self - rhs
}
}
impl MulAssign<i32> for Vec2 {
fn mul_assign(&mut self, rhs: i32) {
*self = *self * rhs
}
}
impl DivAssign<i32> for Vec2 {
fn div_assign(&mut self, rhs: i32) {
*self = *self / rhs
}
}

269
src/main.rs Normal file
View file

@ -0,0 +1,269 @@
use std::{time::{Instant, Duration}, iter::repeat, collections::HashSet};
use data::BOARD;
use pancurses::{start_color, use_default_colors, init_pair, COLOR_CYAN, COLOR_YELLOW, COLOR_MAGENTA, COLOR_GREEN, COLOR_RED, COLOR_BLUE, init_color, COLOR_WHITE, initscr, noecho, curs_set, cbreak, endwin, Input};
use pacman::{Vec2, Direction};
use rand::{prelude::IteratorRandom, thread_rng};
use crate::data::{BOARD_WIDTH, BOARD_HEIGHT};
mod data;
const PACMAN: &str = "()";
const GHOST: &str = "⎧⎫";
const PELLET: &str = "";
const POWER_PELLET: &str = "<>";
const COLOR_ORANGE: i16 = 8;
const BLINKY_HOME: Vec2 = Vec2 { x: 25, y: -5 };
const INKY_HOME: Vec2 = Vec2 { x: 27, y: 31 };
const PINKY_HOME: Vec2 = Vec2 { x: 2, y: -5 };
const CLYDE_HOME: Vec2 = Vec2 { x: 0, y: 31 };
fn init_colors() {
start_color();
use_default_colors();
init_pair(COLOR_CYAN, COLOR_CYAN, -1);
init_pair(COLOR_YELLOW, COLOR_YELLOW, -1);
init_pair(COLOR_MAGENTA, COLOR_MAGENTA, -1);
init_pair(COLOR_GREEN, COLOR_GREEN, -1);
init_pair(COLOR_RED, COLOR_RED, -1);
init_pair(COLOR_BLUE, COLOR_BLUE, -1);
init_color(COLOR_ORANGE, 1000, 843, 0);
init_pair(COLOR_ORANGE, COLOR_ORANGE, -1);
init_pair(COLOR_WHITE, COLOR_WHITE, -1);
}
fn offset_with_bug(pos: Vec2, dir: Direction, dist: i32) -> Vec2 {
pos + match dir {
// in the original game, an offset to the north would also offset to the west
Direction::North => Vec2::from(Direction::North) + Direction::West,
_ => dir.into()
} * dist
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum GhostState {
Chase,
Scatter,
Frightened,
Eaten
}
struct Ghost {
state: GhostState,
pos: Vec2,
target: Vec2,
facing: Direction,
}
impl Ghost {
fn new(pos: Vec2, facing: Direction) -> Self {
Self { state: GhostState::Chase, pos, target: Default::default(), facing }
}
fn get_movement_dir(&self) -> Direction {
// special case that prevents ghosts from turning within a corridor at the bottom
let special_cases: &[Vec2] = &[(12, 23).into(), (15, 23).into(), (12, 11).into(), (15, 11).into()];
if special_cases.contains(&self.pos) && !BOARD[self.pos + self.facing] {
return self.facing;
}
use Direction::*;
// particular order of priority
let options = [North, West, South, East].into_iter() // all directions
.filter(|&v| v != self.facing.flipped()) // that arent backward
.filter(|&v| !BOARD[self.pos + v]); // that arent solid
if self.state == GhostState::Frightened {
return options.choose(&mut thread_rng()).unwrap_or(self.facing.flipped());
}
// with the shortest distance to target
options.min_by_key(|&v| self.target.sq_dist(self.pos + v))
.unwrap_or(self.facing.flipped()) // default to backward
}
fn move_(&mut self) {
self.facing = self.get_movement_dir();
self.pos += self.facing;
self.pos.x = self.pos.x.rem_euclid(BOARD_WIDTH as i32);
self.pos.y = self.pos.y.rem_euclid(BOARD_HEIGHT as i32);
}
fn set_target_and_move(&mut self, if_chase: impl FnOnce(&Self) -> Vec2, home: Vec2) {
self.target = match self.state {
GhostState::Chase => if_chase(self),
GhostState::Scatter => home,
GhostState::Frightened => Default::default(), // doesnt matter
GhostState::Eaten => (13, 11).into(),
};
self.move_();
}
}
fn main() {
let window = initscr();
noecho();
curs_set(0);
cbreak();
init_colors();
window.keypad(true);
window.nodelay(true);
window.refresh();
const TICK_DUR: Duration = Duration::from_millis(160);
let mut blinky = Ghost::new((6,5).into(), Direction::East);
let mut inky = Ghost::new((21,5).into(), Direction::West);
let mut pinky = Ghost::new((6,26).into(), Direction::North);
let mut clyde = Ghost::new((21,26).into(), Direction::North);
let mut pellets = BOARD.0.iter()
.enumerate()
.flat_map(|v| repeat(v.0).zip(v.1.iter().enumerate()))
.filter_map(|v| if *v.1.1 { None } else { Some((v.1.0 as i32, v.0 as i32)) })
.map(Vec2::from)
.collect::<HashSet<_>>();
let mut player_pos: Vec2 = (13, 23).into();
let mut player_dir: Direction = Direction::South;
let mut queued_dir: Option<Direction> = None;
let mut time_till_next_tick = TICK_DUR;
let mut time_till_next_ghost_tick = TICK_DUR;
let mut last_iter = Instant::now();
let mut needs_refresh = true;
let mut lost = false;
let won = loop {
let this_iter = Instant::now();
let delta_time = this_iter - last_iter;
last_iter = this_iter;
time_till_next_tick = time_till_next_tick.saturating_sub(delta_time);
time_till_next_ghost_tick = time_till_next_ghost_tick.saturating_sub(delta_time);
if let Some(key) = window.getch() {
use Input::*;
match key {
KeyUp | KeyDown | KeyLeft | KeyRight => {
queued_dir = Some(match key {
KeyUp => Direction::North,
KeyDown => Direction::South,
KeyRight => Direction::East,
KeyLeft => Direction::West,
_ => unreachable!() // i believe in the compiler to optimize this out
});
},
Character('q') => break false,
_ => {}
}
}
if time_till_next_tick.is_zero() {
time_till_next_tick = TICK_DUR;
pellets.remove(&player_pos);
if let Some(queued_dir_unwrapped) = queued_dir {
let potential_next_pos = player_pos + queued_dir_unwrapped;
if !BOARD[potential_next_pos] {
player_dir = queued_dir_unwrapped;
queued_dir = None;
}
}
let prev_pos = player_pos;
player_pos += player_dir;
player_pos.x = player_pos.x.rem_euclid(BOARD_WIDTH as i32);
player_pos.y = player_pos.y.rem_euclid(BOARD_HEIGHT as i32);
if BOARD[player_pos] {
player_pos = prev_pos;
}
pellets.remove(&player_pos);
if pellets.is_empty() {
break true;
}
if [blinky.pos, inky.pos, pinky.pos, clyde.pos].contains(&player_pos) {
time_till_next_ghost_tick = Duration::MAX; // kinda hacky
lost = true;
}
needs_refresh = true;
}
if time_till_next_ghost_tick.is_zero() {
time_till_next_ghost_tick = TICK_DUR;
blinky.set_target_and_move(|_| player_pos, BLINKY_HOME);
inky.set_target_and_move(|_| {
let intermediate = offset_with_bug(player_pos, player_dir, 2);
intermediate - (blinky.pos - intermediate) // flip blinky pos around intermediate
}, INKY_HOME);
pinky.set_target_and_move(|_| offset_with_bug(player_pos, player_dir, 4), PINKY_HOME);
clyde.set_target_and_move(|clyde| if player_pos.dist(clyde.pos) > 8f32 { player_pos } else { CLYDE_HOME }, CLYDE_HOME);
if [blinky.pos, inky.pos, pinky.pos, clyde.pos].contains(&player_pos) {
lost = true;
}
needs_refresh = true;
}
if needs_refresh {
// board
window.color_set(COLOR_BLUE);
window.mvprintw(0, 0, include_str!("board.txt"));
// pellets
window.color_set(COLOR_YELLOW);
for pellet in &pellets {
window.mvprintw(pellet.y, pellet.x * 2, PELLET);
}
// player
window.mvprintw(player_pos.y, player_pos.x * 2, PACMAN);
// blinky
window.color_set(COLOR_RED);
window.mvprintw(blinky.pos.y, blinky.pos.x * 2, GHOST);
// pinky
window.color_set(COLOR_MAGENTA);
window.mvprintw(pinky.pos.y, pinky.pos.x * 2, GHOST);
// inky
window.color_set(COLOR_CYAN);
window.mvprintw(inky.pos.y, inky.pos.x * 2, GHOST);
// clyde
window.color_set(COLOR_GREEN);
window.mvprintw(clyde.pos.y, clyde.pos.x * 2, GHOST);
// debug stuff
window.color_set(COLOR_WHITE);
window.mvprintw(0, 57, format!("{}, {}", player_pos.x, player_pos.y));
needs_refresh = false;
}
if lost {
break false;
}
};
endwin();
std::process::exit(if won { 0 } else { 1 });
}
#[cfg(target_os = "windows")]
compile_error!("diagnosis: skill issue");