Compare commits

...

No commits in common. "rewrite" and "xplatform-refactor-2" have entirely different histories.

40 changed files with 2680 additions and 1507 deletions

View file

@ -1,2 +0,0 @@
[alias]
run-wasm = "run --release --package run-wasm --"

1
.envrc
View file

@ -1 +0,0 @@
# use flake

15
.gitignore vendored
View file

@ -1,17 +1,2 @@
/target /target
**/*.rs.bk **/*.rs.bk
.direnv
# Added by cargo
#
# already existing elements were commented out
#/target
# Added by cargo
#
# already existing elements were commented out
#/target

1395
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -1,13 +1,19 @@
[package] [package]
name = "wedge" name = "wedge"
version = "0.1.0" version = "0.1.0"
edition = "2021" authors = ["Michael Zhang <iptq@protonmail.com>"]
edition = "2018"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [profile.release]
opt-level = 'z'
lto = true
panic = 'abort'
[dependencies] [dependencies]
anyhow = "1.0.79" glium = "0.25"
json5 = "0.4.1" image = "0.21"
macroquad = "0.4.4" json5 = "0.2"
serde = { version = "1.0.195", features = ["derive"] } nalgebra = "0.18"
serde_json = "1.0.111" nalgebra-glm = "0.4"
serde = "1.0"
serde_derive = "1.0"

19
LICENSE Normal file
View file

@ -0,0 +1,19 @@
Copyright (c) Michael Zhang
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

38
README.md Normal file
View file

@ -0,0 +1,38 @@
wedge: a puzzle game
====================
![screenshot](screenshot.jpg?raw=true)
Mechanics:
- **Objective:** Get the two players to reach the goals by navigating the level and pushing blocks around.
- Blocks with triangular sections will push other blocks in the other direction.
- Blocks of the same color will always move together.
Controls: WASD for the left player and IJKL for the right player
To-do list
----------
- [ ] Gameplay
- [x] Collision algorithm
- [x] Primitive animations
- [ ] Orientation indicator
- [ ] Unique textures
- [ ] Cosmetics
- [ ] A real menu interface
- [ ] In-game editor
Credits
-------
Original game made during MinneHack 2019: https://github.com/iptq/planar
- Yeshi Cai
- Mark Pekala
- Alex Shi
- Michael Zhang
Rewrite: Michael Zhang
License: MIT

View file

@ -1,12 +0,0 @@
{ rustc, cargo, makeRustPlatform, cmake, pkg-config, fontconfig }:
let rustPlatform = makeRustPlatform { inherit cargo rustc; };
in rustPlatform.buildRustPackage {
name = "wedge";
src = ./.;
cargoLock.lockFile = ./Cargo.lock;
nativeBuildInputs = [ cmake pkg-config ];
buildInputs = [ fontconfig ];
}

View file

@ -1,97 +0,0 @@
{
"nodes": {
"fenix": {
"inputs": {
"nixpkgs": [
"nixpkgs"
],
"rust-analyzer-src": "rust-analyzer-src"
},
"locked": {
"lastModified": 1706163833,
"narHash": "sha256-Vw+jTVtKceT+ScaIn7tHy8JjRZZpmg2fAdoInLAsW/M=",
"owner": "nix-community",
"repo": "fenix",
"rev": "043f63f55e9c9b808852ea82edee1f2a1af37e91",
"type": "github"
},
"original": {
"owner": "nix-community",
"repo": "fenix",
"type": "github"
}
},
"flake-utils": {
"inputs": {
"systems": "systems"
},
"locked": {
"lastModified": 1705309234,
"narHash": "sha256-uNRRNRKmJyCRC/8y1RqBkqWBLM034y4qN7EprSdmgyA=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "1ef2e671c3b0c19053962c07dbda38332dcebf26",
"type": "github"
},
"original": {
"id": "flake-utils",
"type": "indirect"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1706006310,
"narHash": "sha256-nDPz0fj0IFcDhSTlXBU2aixcnGs2Jm4Zcuoj0QtmiXQ=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "b43bb235efeab5324c5e486882ef46749188eee2",
"type": "github"
},
"original": {
"id": "nixpkgs",
"type": "indirect"
}
},
"root": {
"inputs": {
"fenix": "fenix",
"flake-utils": "flake-utils",
"nixpkgs": "nixpkgs"
}
},
"rust-analyzer-src": {
"flake": false,
"locked": {
"lastModified": 1706106882,
"narHash": "sha256-31DivWu0cC50gR2CgbGtLCf77nuiw4kdiI7B8ioqzLw=",
"owner": "rust-lang",
"repo": "rust-analyzer",
"rev": "0d52934d19d7addcafcfda92a1d547b51556beec",
"type": "github"
},
"original": {
"owner": "rust-lang",
"ref": "nightly",
"repo": "rust-analyzer",
"type": "github"
}
},
"systems": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
}
},
"root": "root",
"version": 7
}

View file

@ -1,64 +0,0 @@
{
inputs = {
fenix = {
url = "github:nix-community/fenix";
inputs.nixpkgs.follows = "nixpkgs";
};
};
outputs = { self, nixpkgs, flake-utils, fenix }:
flake-utils.lib.eachDefaultSystem (system:
let
pkgs = import nixpkgs {
inherit system;
overlays = [ fenix.overlays.default ];
};
rustc = pkgs.fenix.stable.rustc;
cargo = pkgs.fenix.stable.cargo;
neededLibs = with pkgs;
(with xorg; [ ])
++ [ ];
flakePkgs = { wedge = pkgs.callPackage ./. { inherit rustc cargo; }; };
in
rec {
packages = flake-utils.lib.flattenTree flakePkgs;
defaultPackage = packages.wedge;
devShell = pkgs.mkShell {
inputsFrom = with packages; [ wedge ];
shellHook = ''
export LD_LIBRARY_PATH="$LD_LIBRARY_PATH:${
pkgs.lib.makeLibraryPath neededLibs
}"
'';
packages = (with pkgs; [
# cargo-watch
# cargo-deny
# cargo-edit
# sqlx-cli
# sqlite
(with pkgs.fenix;
combine [
rustc
cargo
# targets.wasm32-unknown-unknown.latest.rust-std
])
]);
PKG_CONFIG_PATH = with pkgs;
lib.concatStringsSep ":" [
# "${fontconfig.dev}/lib/pkgconfig"
# "${xorg.libX11.dev}/lib/pkgconfig"
# "${xorg.libXcursor.dev}/lib/pkgconfig"
# "${xorg.libXi.dev}/lib/pkgconfig"
# "${xorg.libXrandr.dev}/lib/pkgconfig"
];
};
});
}

View file

@ -1,4 +0,0 @@
max_width = 80
tab_spaces = 2
wrap_comments = true
fn_single_line = true

BIN
screenshot.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

22
shaders/cell.frag Normal file
View file

@ -0,0 +1,22 @@
#version 330
in vec4 pos;
out vec4 outcolor;
uniform vec4 color;
const float threshold = 0.05;
const vec4 top = vec4(0.5, 0.5, 0.5, 1.0);
const vec4 bot = vec4(0.4, 0.4, 0.4, 1.0);
void main() {
outcolor = vec4(0.2 * (0.4 * (1 - pos.y) + 0.5 * (1 - pos.x)) + 0.2);
outcolor.w = 1.0;
// if ((pos.x > -threshold && pos.x < threshold)
// || (pos.y > -threshold && pos.y < threshold)
// || (pos.x > 1.0-threshold && pos.x < 1.0+threshold)
// || (pos.y > 1.0-threshold && pos.y < 1.0+threshold)) {
// outcolor = vec4(0.0, 0.0, 0.0, 1.0);
// }
}

12
shaders/cell.vert Normal file
View file

@ -0,0 +1,12 @@
#version 330
in vec2 point;
out vec4 pos;
uniform mat4 target;
uniform mat4 projection;
void main() {
pos = vec4(point, 0.0, 1.0);
gl_Position = projection * target * pos;
}

11
shaders/segment.frag Normal file
View file

@ -0,0 +1,11 @@
#version 330
in vec2 v_tex_coords;
out vec4 outcolor;
uniform sampler2D tex;
uniform vec4 tint;
void main() {
outcolor = tint * texture(tex, v_tex_coords);
}

13
shaders/segment.vert Normal file
View file

@ -0,0 +1,13 @@
#version 330
in vec2 pos;
in vec2 tex;
out vec2 v_tex_coords;
uniform mat4 target;
uniform mat4 projection;
void main() {
v_tex_coords = tex;
gl_Position = projection * target * vec4(pos, 0.0, 1.0);
}

View file

@ -1,15 +1,14 @@
use std::collections::{HashMap, HashSet}; use std::collections::{HashMap, HashSet};
use std::time::Duration; use std::time::Duration;
use crate::game_state::level::{ChangeSet, Entity, FailSet}; use crate::enums::Board;
use crate::game_state::Board; use crate::level::{ChangeSet, Entity, FailSet};
pub type MoveResult = Result<ChangeSet, FailSet>; pub type MoveResult = Result<ChangeSet, FailSet>;
pub type BlockOffsets = HashMap<Entity, (f32, f32)>; pub type BlockOffsets = HashMap<Entity, (f32, f32)>;
// TODO: don't yeet around a HashMap all the time // TODO: don't yeet around a HashMap all the time
pub type AnimationFn = pub type AnimationFn = Box<dyn Fn(MoveResult, BlockOffsets, f32) -> BlockOffsets>;
Box<dyn Fn(MoveResult, BlockOffsets, f32) -> BlockOffsets>;
// in seconds // in seconds
const ANIMATION_DURATION: f32 = 1.0 / 6.0; const ANIMATION_DURATION: f32 = 1.0 / 6.0;
@ -42,9 +41,7 @@ impl AnimationState {
self.is_animating = true; self.is_animating = true;
self.progress = 0.0; self.progress = 0.0;
let func = |last_move_result: MoveResult, let func = |last_move_result: MoveResult, mut offsets: BlockOffsets, progress: f32| {
mut offsets: BlockOffsets,
progress: f32| {
use std::f32::consts::PI; use std::f32::consts::PI;
match last_move_result { match last_move_result {
// transition // transition
@ -73,8 +70,7 @@ impl AnimationState {
} }
pub fn make_progress(&mut self, delta: Duration) { pub fn make_progress(&mut self, delta: Duration) {
let progress = let progress = self.progress + (delta.as_millis() as f32 / ANIMATION_DURATION) / 1000.0;
self.progress + (delta.as_millis() as f32 / ANIMATION_DURATION) / 1000.0;
let block_offsets = if let Some(f) = &self.progress_function { let block_offsets = if let Some(f) = &self.progress_function {
Some(f( Some(f(
@ -99,16 +95,14 @@ impl AnimationState {
} }
pub fn get_block_offset(&self, index: usize) -> (f32, f32) { pub fn get_block_offset(&self, index: usize) -> (f32, f32) {
self self.block_offsets
.block_offsets
.get(&Entity::Block(index)) .get(&Entity::Block(index))
.cloned() .cloned()
.unwrap_or_else(|| (0.0, 0.0)) .unwrap_or_else(|| (0.0, 0.0))
} }
pub fn get_player_offset(&self, board: Board) -> (f32, f32) { pub fn get_player_offset(&self, board: Board) -> (f32, f32) {
self self.block_offsets
.block_offsets
.get(&Entity::Player(board)) .get(&Entity::Player(board))
.cloned() .cloned()
.unwrap_or_else(|| (0.0, 0.0)) .unwrap_or_else(|| (0.0, 0.0))

14
src/color.rs Normal file
View file

@ -0,0 +1,14 @@
#[derive(Copy, Clone, Debug)]
pub struct Color(pub f32, pub f32, pub f32, pub f32);
impl Color {
pub fn from_rgb_u32(r: u32, g: u32, b: u32) -> Self {
Color(r as f32 / 256.0, g as f32 / 256.0, b as f32 / 256.0, 1.0)
}
}
impl From<(u32, u32, u32)> for Color {
fn from(tuple: (u32, u32, u32)) -> Self {
Color::from_rgb_u32(tuple.0, tuple.1, tuple.2)
}
}

23
src/data.rs Normal file
View file

@ -0,0 +1,23 @@
#[derive(Debug, Deserialize)]
pub struct PlayerData {
pub position: (i32, i32),
pub color: (u32, u32, u32),
}
#[derive(Debug, Deserialize)]
pub struct BlockData {
pub movable: bool,
pub orientation: u32,
pub color: (u32, u32, u32),
pub segments: Vec<[i32; 4]>,
}
#[derive(Debug, Deserialize)]
pub struct LevelData {
pub dimensions: [u32; 2],
pub player1: PlayerData,
pub player2: PlayerData,
pub goal1: (i32, i32),
pub goal2: (i32, i32),
pub blocks: Vec<BlockData>,
}

View file

@ -1 +0,0 @@
pub fn draw() {}

109
src/enums.rs Normal file
View file

@ -0,0 +1,109 @@
use std::ops::Add;
#[derive(Debug, Eq, PartialEq, Hash, PartialOrd, Copy, Clone)]
pub enum Board {
Left = 0,
Right = 1,
}
impl From<i32> for Board {
fn from(n: i32) -> Self {
match n {
0 => Board::Left,
1 => Board::Right,
_ => panic!("expecting 0 or 1, got {}", n),
}
}
}
#[derive(Copy, Clone, Debug)]
pub enum Orientation {
None = 0,
Horizontal = 1,
Vertical = 2,
Both = 3,
}
impl From<u32> for Orientation {
fn from(n: u32) -> Self {
match n {
0 => Orientation::Both,
1 => Orientation::Horizontal,
2 => Orientation::Vertical,
_ => panic!("expecting 0..2, got {}", n),
}
}
}
#[derive(Copy, Clone, Debug, Hash, PartialEq, Eq)]
pub enum PushDir {
Up,
Down,
Left,
Right,
}
impl PushDir {
pub fn as_pair(self) -> (i32, i32) {
match self {
PushDir::Up => (0, -1),
PushDir::Down => (0, 1),
PushDir::Left => (-1, 0),
PushDir::Right => (1, 0),
}
}
}
impl Add<PushDir> for (i32, i32, Board) {
type Output = (i32, i32, Board);
fn add(self, rhs: PushDir) -> Self::Output {
let offset = rhs.as_pair();
(self.0 + offset.0, self.1 + offset.1, self.2)
}
}
// /\
// /21\
// \34/
// \/
#[derive(Copy, Clone, Debug, PartialOrd, PartialEq)]
pub enum Shape {
Full = 0,
TopRight = 1,
TopLeft = 2,
BottomLeft = 3,
BottomRight = 4,
}
impl From<i32> for Shape {
fn from(n: i32) -> Self {
match n {
0 => Shape::Full,
1 => Shape::TopRight,
2 => Shape::TopLeft,
3 => Shape::BottomLeft,
4 => Shape::BottomRight,
_ => panic!("expecting 0..4, got {}", n),
}
}
}
impl Shape {
pub fn get_opposite(self) -> Option<Shape> {
use Shape::*;
match self {
TopRight => Some(BottomLeft),
BottomLeft => Some(TopRight),
TopLeft => Some(BottomRight),
BottomRight => Some(TopLeft),
Full => None,
}
}
pub fn is_opposite(self, other: Shape) -> bool {
self.get_opposite()
.map(|shape| shape == other)
.unwrap_or_else(|| false)
}
}

78
src/game.rs Normal file
View file

@ -0,0 +1,78 @@
use std::time::Duration;
use glium::glutin::{ElementState, Event, WindowEvent};
use glium::{Display, Frame};
use crate::keymap::Keymap;
use crate::renderer::Renderer;
use crate::resources::Resources;
use crate::screens::{MenuScreen, ScreenStack};
const SEGMENT_VERT: &str = include_str!("../shaders/segment.vert");
const SEGMENT_FRAG: &str = include_str!("../shaders/segment.frag");
const CELL_VERT: &str = include_str!("../shaders/cell.vert");
const CELL_FRAG: &str = include_str!("../shaders/cell.frag");
const SEGMENT_IMAGE: &[u8] = include_bytes!("../textures/segment.png");
pub struct Game<'a> {
pub resources: Resources,
pub display: &'a Display,
keymap: Keymap,
screen_stack: ScreenStack,
}
impl<'a> Game<'a> {
pub fn new(display: &'a Display) -> Game {
let mut resources = Resources::default();
resources
.load_image_from_memory(display, "segment", &SEGMENT_IMAGE, false)
.unwrap();
resources
.load_shader(display, "segment", &SEGMENT_VERT, &SEGMENT_FRAG)
.unwrap();
resources
.load_shader(display, "cell", &CELL_VERT, &CELL_FRAG)
.unwrap();
// bruh
let screen_stack = ScreenStack::with(MenuScreen::new());
Game {
resources,
display,
keymap: Keymap::new(),
screen_stack,
}
}
pub fn handle_event(&mut self, event: Event) {
if let Event::WindowEvent { event, .. } = event {
match event {
WindowEvent::Resized(size) => self.resources.window_dimensions = size.into(),
WindowEvent::KeyboardInput { input, .. } => {
if let Some(code) = &input.virtual_keycode {
if let ElementState::Pressed = &input.state {
self.keymap.pressed(*code);
} else {
self.keymap.release(*code);
}
}
}
_ => (),
}
}
}
pub fn create_renderer<'b>(&self, target: &'b mut Frame) -> Renderer<'b, '_> {
Renderer::new(self, target)
}
pub fn update(&mut self, delta: Duration) {
self.screen_stack.update(delta, &self.keymap);
}
pub fn render(&self, renderer: &mut Renderer) {
self.screen_stack.render(renderer);
}
}

View file

@ -1,35 +0,0 @@
use super::{BlockData, Color, Orientation, SegmentState};
#[derive(Clone, Debug)]
pub struct BlockState {
pub index: usize,
pub movable: bool,
pub color: Color,
pub orientation: Orientation,
pub segments: Vec<SegmentState>,
}
impl BlockState {
pub fn from_data(index: usize, data: &BlockData) -> Self {
let movable = data.movable;
let segments = data
.segments
.iter()
.map(|segment| SegmentState {
position: (segment[0], segment[1]),
shape: segment[2].into(),
board: segment[3].into(),
})
.collect();
let orientation = data.orientation.into();
let color = Color(data.color.0, data.color.1, data.color.2);
BlockState {
index,
movable,
color,
segments,
orientation,
}
}
}

View file

@ -1,48 +0,0 @@
use std::collections::{HashMap, HashSet};
use super::{Board, LevelState, PushDir, Shape};
#[derive(Copy, Clone, Debug, Hash, PartialEq, Eq)]
pub enum Entity {
Block(usize),
Player(Board),
}
pub type ChangeSet = HashMap<Entity, PushDir>;
pub type FailSet = HashSet<usize>;
impl LevelState {
pub fn apply_change_set(&mut self, change_set: ChangeSet) {
for (entity, direction) in change_set {
let direction = direction.as_pair();
match entity {
Entity::Player(board) => {
let player = match board {
Board::Left => &mut self.player1_position,
Board::Right => &mut self.player2_position,
};
player.0 += direction.0;
player.1 += direction.1;
}
Entity::Block(index) => {
let block = self.blocks.get_mut(index).expect("big failure");
for segment in &mut block.segments {
segment.position.0 += direction.0;
segment.position.1 += direction.1;
}
}
}
}
}
pub fn try_move(
&mut self,
board: Board,
direction: PushDir,
) -> Result<ChangeSet, FailSet> {
let mut change_set = ChangeSet::default();
change_set.insert(Entity::Player(board), direction);
self.player_can_move(board, direction, change_set)
}
}

View file

@ -1,197 +0,0 @@
pub mod block;
pub mod level;
pub mod move_rules;
pub mod render;
use std::ops::Add;
use anyhow::Result;
use macroquad::color::Color as MQColor;
use self::block::BlockState;
#[derive(Debug)]
pub struct LevelState {
pub data: LevelData,
pub blocks: Vec<BlockState>,
pub player1_position: (i32, i32),
pub player2_position: (i32, i32),
}
#[derive(Copy, Clone, Debug, PartialOrd, PartialEq)]
pub struct SegmentState {
position: (i32, i32),
shape: Shape,
board: Board,
}
impl LevelState {
pub fn new(data: LevelData) -> Self {
let blocks = data
.blocks
.iter()
.enumerate()
.map(|(i, block)| BlockState::from_data(i, block))
.collect();
LevelState {
blocks,
player1_position: data.player1.position,
player2_position: data.player2.position,
data,
}
}
pub fn check_win_condition(&self) -> bool {
self.player1_position == self.data.goal1
&& self.player2_position == self.data.goal2
}
}
#[derive(Clone, Copy, Debug, Serialize, Deserialize)]
pub struct Color(u8, u8, u8);
impl Into<MQColor> for Color {
fn into(self) -> MQColor {
MQColor::from_rgba(self.0, self.1, self.2, 255)
}
}
#[derive(Debug, Serialize, Deserialize)]
pub struct PlayerData {
pub position: (i32, i32),
pub color: Color,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct BlockData {
pub movable: bool,
pub orientation: u32,
pub color: Color,
pub segments: Vec<[i32; 4]>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct LevelData {
pub dimensions: [u32; 2],
pub player1: PlayerData,
pub player2: PlayerData,
pub goal1: (i32, i32),
pub goal2: (i32, i32),
pub blocks: Vec<BlockData>,
}
impl LevelData {
pub fn load_from_string(string: &str) -> Result<Self> {
json5::from_str(string).map_err(|err| err.into())
}
}
#[derive(Debug, Eq, PartialEq, Hash, PartialOrd, Copy, Clone)]
pub enum Board {
Left = 0,
Right = 1,
}
impl From<i32> for Board {
fn from(n: i32) -> Self {
match n {
0 => Board::Left,
1 => Board::Right,
_ => panic!("expecting 0 or 1, got {}", n),
}
}
}
#[derive(Copy, Clone, Debug)]
pub enum Orientation {
None = 0,
Horizontal = 1,
Vertical = 2,
Both = 3,
}
impl From<u32> for Orientation {
fn from(n: u32) -> Self {
match n {
0 => Orientation::Both,
1 => Orientation::Horizontal,
2 => Orientation::Vertical,
_ => panic!("expecting 0..2, got {}", n),
}
}
}
#[derive(Copy, Clone, Debug, Hash, PartialEq, Eq)]
pub enum PushDir {
Up,
Down,
Left,
Right,
}
impl PushDir {
pub fn as_pair(self) -> (i32, i32) {
match self {
PushDir::Up => (0, -1),
PushDir::Down => (0, 1),
PushDir::Left => (-1, 0),
PushDir::Right => (1, 0),
}
}
}
impl Add<PushDir> for (i32, i32, Board) {
type Output = (i32, i32, Board);
fn add(self, rhs: PushDir) -> Self::Output {
let offset = rhs.as_pair();
(self.0 + offset.0, self.1 + offset.1, self.2)
}
}
// /\
// /21\
// \34/
// \/
#[derive(Copy, Clone, Debug, PartialOrd, PartialEq)]
pub enum Shape {
Full = 0,
TopRight = 1,
TopLeft = 2,
BottomLeft = 3,
BottomRight = 4,
}
impl From<i32> for Shape {
fn from(n: i32) -> Self {
match n {
0 => Shape::Full,
1 => Shape::TopRight,
2 => Shape::TopLeft,
3 => Shape::BottomLeft,
4 => Shape::BottomRight,
_ => panic!("expecting 0..4, got {}", n),
}
}
}
impl Shape {
pub fn get_opposite(self) -> Option<Shape> {
use Shape::*;
match self {
TopRight => Some(BottomLeft),
BottomLeft => Some(TopRight),
TopLeft => Some(BottomRight),
BottomRight => Some(TopLeft),
Full => None,
}
}
pub fn is_opposite(self, other: Shape) -> bool {
self
.get_opposite()
.map(|shape| shape == other)
.unwrap_or_else(|| false)
}
}

View file

@ -1,245 +0,0 @@
use std::collections::HashSet;
use crate::game_state::{level::Entity, Board, Orientation, PushDir, Shape};
use super::{
level::{ChangeSet, FailSet},
LevelState, SegmentState,
};
impl LevelState {
pub fn player_can_move(
&self,
board: Board,
direction: PushDir,
change_set: ChangeSet,
) -> Result<ChangeSet, FailSet> {
let player_position = match board {
Board::Left => &self.player1_position,
Board::Right => &self.player2_position,
};
let player_segment = SegmentState {
position: *player_position,
shape: Shape::Full,
board,
};
self.segment_can_move(None, player_segment, direction, change_set)
}
fn block_can_move(
&self,
index: usize,
direction: PushDir,
mut change_set: ChangeSet,
) -> Result<ChangeSet, FailSet> {
println!("block_can_move({:?}, {:?})", index, direction);
let block = match self.blocks.get(index) {
Some(block) => block,
None => return Err(HashSet::new()),
};
// is the block even movable?
if !block.movable {
return Err(set!(index));
}
// does the direction match the orientation?
match (block.orientation, direction) {
(Orientation::Horizontal, PushDir::Left)
| (Orientation::Horizontal, PushDir::Right)
| (Orientation::Vertical, PushDir::Up)
| (Orientation::Vertical, PushDir::Down)
| (Orientation::Both, _) => (),
_ => return Err(set!(index)),
}
// TODO: change this to use &mut instead of returning a new one each time
change_set.insert(Entity::Block(index), direction);
for segment in block.segments.iter() {
match self.segment_can_move(
Some(index),
segment.clone(),
direction,
change_set.clone(),
) {
Ok(new_change_set) => change_set = new_change_set,
Err(fail_set) => return Err(fail_set),
}
}
Ok(change_set)
}
fn segment_can_move(
&self,
block_index: Option<usize>,
segment: SegmentState,
direction: PushDir,
change_set: ChangeSet,
) -> Result<ChangeSet, FailSet> {
println!(
"segment_can_move({:?}, {:?}, {:?})",
block_index, segment, direction
);
let segment_loc = (segment.position.0, segment.position.1, segment.board);
let target = segment_loc + direction;
println!(" - target: {:?}", target);
// is the target actually in the map?
if target.0 < 0
|| target.0 >= self.data.dimensions[0] as i32
|| target.1 < 0
|| target.1 >= self.data.dimensions[1] as i32
{
return Err(entity_fail!(block_index));
}
// retrieve other blocks that might be occupying this current space and the target space
let mut current_occupant = None;
let mut target_occupant = None;
for (i, block) in self.blocks.iter().enumerate() {
// skip other segments of the same block
if let Some(n) = block_index {
if n == i {
continue;
}
}
// offset from the change set
let offset = match change_set.get(&Entity::Block(i)) {
Some(direction) => direction.as_pair(),
None => (0, 0),
};
for segment in block.segments.iter() {
// don't get segments on different boards
if segment.board != segment_loc.2 {
continue;
}
let mut segment_pos = segment.position;
if segment_pos == (segment_loc.0, segment_loc.1) {
current_occupant = Some((i, segment.shape, block.orientation));
}
segment_pos.0 += offset.0;
segment_pos.1 += offset.1;
if segment_pos == (target.0, target.1) {
target_occupant =
Some((Entity::Block(i), segment.shape, block.orientation));
}
}
}
// check if the target occupant is actually a player
if let None = target_occupant {
if segment.board == Board::Left
&& self.player1_position == (target.0, target.1)
{
target_occupant =
Some((Entity::Player(Board::Left), Shape::Full, Orientation::None));
} else if segment.board == Board::Right
&& self.player2_position == (target.0, target.1)
{
target_occupant =
Some((Entity::Player(Board::Right), Shape::Full, Orientation::None));
}
}
println!(
" - occupants: current={:?} | target={:?}",
current_occupant, target_occupant
);
// handle special pushes
if let Some((other_block, other_shape, other_orientation)) =
current_occupant
{
// are both shapes triangles?
let both_triangles = match (segment.shape, other_shape) {
(Shape::Full, Shape::Full) => false,
(Shape::Full, _) => unreachable!("invalid to have triangle + full"),
(_, Shape::Full) => unreachable!("invalid to have triangle + full"),
_ => true,
};
if both_triangles {
// what directions could we be pushing the other block into?
let possible_directions = match segment.shape {
Shape::TopRight => [PushDir::Up, PushDir::Right],
Shape::TopLeft => [PushDir::Left, PushDir::Up],
Shape::BottomLeft => [PushDir::Down, PushDir::Left],
Shape::BottomRight => [PushDir::Right, PushDir::Down],
Shape::Full => unreachable!("already eliminated this possibility"),
};
println!(" - possible directions: {:?}", possible_directions);
// does the direction we're pushing appear in this list?
if possible_directions.contains(&direction) {
// the other shape goes in the other direction
let other_direction = match other_orientation {
Orientation::None => {
unreachable!("already eliminated this possibility")
}
Orientation::Vertical => [PushDir::Up, PushDir::Down],
Orientation::Horizontal => [PushDir::Left, PushDir::Right],
Orientation::Both => unimplemented!(),
};
let possible_directions =
possible_directions.iter().collect::<HashSet<_>>();
let other_direction = other_direction.iter().collect();
let mut intersected_direction =
possible_directions.intersection(&other_direction);
let new_direction = **intersected_direction.next().unwrap();
// let other_direction = {
// let mut set = possible_directions.iter().collect::<HashSet<_>>();
// set.remove(&direction);
// *set.into_iter().next().unwrap()
// };
let mut result =
self.block_can_move(other_block, new_direction, change_set);
if let Ok(ref mut change_set) = result {
change_set.insert(Entity::Block(other_block), new_direction);
}
return result;
}
}
}
// handle normal pushes
if let Some((entity, shape, _orientation)) = target_occupant {
match entity {
Entity::Player(_) => {
// TODO: assert that the board is the same
Err(fail_set!(change_set))
}
Entity::Block(index) => {
if
// if it's part of the same block it's ok to push
block_index.is_some() && block_index.unwrap() == index ||
// if the shapes are opposite, we can actually both fit into the same spot
segment.shape.is_opposite(shape)
{
Ok(change_set)
}
// if the block is already in the change set, it can't move
else if change_set.contains_key(&Entity::Block(index)) {
Err(fail_set!(change_set))
}
// if the next block can move then so can this one
else {
self.block_can_move(index, direction, change_set)
}
}
}
} else {
// coast is clear, push away!
Ok(change_set)
}
}
}

View file

@ -1,183 +0,0 @@
use macroquad::color::Color as MQColor;
use macroquad::math::{vec2, Vec2};
use macroquad::{
shapes::draw_rectangle,
window::{screen_height, screen_width},
};
use crate::animations::AnimationState;
use super::{Board, Color, LevelState, Orientation, Shape};
impl LevelState {
pub fn render(&self, animations: &AnimationState) {
let width = screen_width();
let height = screen_height();
// board positioning calculations
let playfield_ratio = (2 * self.data.dimensions[0] + 6) as f32
/ (self.data.dimensions[1] + 4) as f32;
let screen_ratio = width / height;
let cols = self.data.dimensions[0] as i32;
let rows = self.data.dimensions[1] as i32;
let (scale, xoff, yoff) = if playfield_ratio > screen_ratio {
let scale = width as f32 / (2 * cols + 6) as f32;
let yoff = height as f32 / 2.0 - (rows + 4) as f32 * scale / 2.0;
(scale, 0.0, yoff)
} else {
let scale = height as f32 / (rows + 4) as f32;
let xoff = width as f32 / 2.0 - (2 * cols + 6) as f32 * scale / 2.0;
(scale, xoff, 0.0)
};
let left_offset = vec2(xoff, yoff);
self.render_boards(scale, left_offset, animations);
}
fn render_boards(
&self,
scale: f32,
offset: Vec2,
animations: &AnimationState,
) {
let left_off = (offset.0 + 2 * scale, offset.1 + 2 * scale);
let right_off = (
offset.0 + (4 + self.data.dimensions[0] as i32) * scale,
offset.1 + 2 * scale,
);
// render the grid
// TODO: do this in one single pass instead of once for each cell
for x in 0..self.data.dimensions[0] as i32 {
for y in 0..self.data.dimensions[1] as i32 {
self
.render_cell((left_off.0 + x * scale, left_off.1 + y * scale), scale);
self.render_cell(
(right_off.0 + x * scale, right_off.1 + y * scale),
scale,
);
}
}
// render blocks
for (i, block) in self.blocks.iter().enumerate() {
for segment in block.segments.iter() {
let offset = match &segment.board {
Board::Left => left_off,
Board::Right => right_off,
};
let mut location = (
offset.0 + segment.position.0 * scale,
offset.1 + segment.position.1 * scale,
);
let animation_offset = animations.get_block_offset(i);
location.0 += (animation_offset.0 * scale as f32) as i32;
location.1 += (animation_offset.1 * scale as f32) as i32;
self.render_segment(
location,
scale,
block.color,
block.orientation,
segment.shape,
);
}
}
// render goals
self.render_goal(self.data.goal1, scale, left_off);
self.render_goal(self.data.goal2, scale, right_off);
// render player
self.render_player(
Board::Left,
self.player1_position,
self.data.player1.color,
scale,
animations,
left_off,
);
self.render_player(
Board::Right,
self.player2_position,
self.data.player1.color,
scale,
animations,
right_off,
);
}
fn render_player(
&self,
board: Board,
player_position: (i32, i32),
player_color: Color,
scale: i32,
animations: &AnimationState,
offset: (i32, i32),
) {
let mut location = (
offset.0 + player_position.0 * scale + 4,
offset.1 + player_position.1 * scale + 4,
);
let animation_offset = animations.get_player_offset(board);
location.0 += (animation_offset.0 * scale as f32) as i32;
location.1 += (animation_offset.1 * scale as f32) as i32;
self.render_segment(
location,
scale - 8,
player_color,
Orientation::Both,
Shape::Full,
);
}
fn render_goal(&self, location: (i32, i32), scale: f32, offset: (i32, i32)) {
let position = (
offset.0 + location.0 * scale + 4,
offset.1 + location.1 * scale + 4,
);
self.render_segment(
position,
scale - 8,
Color(102, 204, 102),
Orientation::Both,
Shape::Full,
);
}
fn render_cell(&self, location: (i32, i32), scale: f32) {
let new_x = location.0 * scale;
let new_y = location.1 * scale;
let color = MQColor::from_rgba(153, 153, 153, 255);
draw_rectangle(
new_x as f32,
new_y as f32,
scale as f32,
scale as f32,
color.into(),
)
}
fn render_segment(
&self,
location: (i32, i32),
scale: i32,
color: Color,
orientation: Orientation,
shape: Shape,
) {
// draw_circle(
// location.0 as f32,
// location.1 as f32,
// scale as f32 * 0.8,
// color.into(),
// );
}
}

27
src/keymap.rs Normal file
View file

@ -0,0 +1,27 @@
use std::collections::HashMap;
use glium::glutin::VirtualKeyCode;
pub struct Keymap(HashMap<VirtualKeyCode, bool>);
impl Keymap {
pub fn new() -> Self {
Keymap(HashMap::new())
}
pub fn pressed(&mut self, code: VirtualKeyCode) {
self.0.insert(code, true);
}
pub fn release(&mut self, code: VirtualKeyCode) {
self.0.insert(code, false);
}
pub fn is_pressed(&self, code: VirtualKeyCode) -> bool {
if let Some(true) = self.0.get(&code) {
true
} else {
false
}
}
}

60
src/level/block.rs Normal file
View file

@ -0,0 +1,60 @@
use crate::color::Color;
use crate::data::BlockData;
use crate::enums::Orientation;
use crate::level::Segment;
pub trait Blockish {
fn get_color(&self) -> Color;
fn get_orientation(&self) -> Orientation;
// TODO: don't alloc/clone here?
fn get_segments(&self) -> Vec<Segment>;
}
#[derive(Clone)]
pub struct Block {
index: usize,
pub movable: bool,
color: Color,
pub orientation: Orientation,
pub segments: Vec<Segment>,
}
impl Block {
pub fn from_data(index: usize, data: &BlockData) -> Self {
let movable = data.movable;
let segments = data
.segments
.iter()
.map(|segment| Segment {
position: (segment[0], segment[1]),
shape: segment[2].into(),
board: segment[3].into(),
})
.collect();
let orientation = data.orientation.into();
let color = Color::from_rgb_u32(data.color.0, data.color.1, data.color.2);
Block {
index,
movable,
color,
segments,
orientation,
}
}
}
impl Blockish for Block {
fn get_color(&self) -> Color {
self.color
}
fn get_orientation(&self) -> Orientation {
self.orientation
}
fn get_segments(&self) -> Vec<Segment> {
self.segments.clone()
}
}

30
src/level/macros.rs Normal file
View file

@ -0,0 +1,30 @@
macro_rules! set {
($($item:expr)*) => {
{
let mut set = std::collections::HashSet::new();
$(set.insert($item);)*
set
}
}
}
macro_rules! fail_set {
($change_set:expr) => {
$change_set
.iter()
.filter_map(|(entity, _)| match entity {
Entity::Block(index) => Some(*index),
Entity::Player(_) => None,
})
.collect()
};
}
macro_rules! entity_fail {
($item:expr) => {
match $item {
Some(index) => set!(index),
None => std::collections::HashSet::new(),
}
};
}

465
src/level/mod.rs Normal file
View file

@ -0,0 +1,465 @@
#[macro_use]
mod macros;
mod block;
mod player;
use std::collections::{HashMap, HashSet, VecDeque};
use crate::animations::AnimationState;
use crate::color::Color;
use crate::data::LevelData;
use crate::enums::{Board, Orientation, PushDir, Shape};
use crate::renderer::Renderer;
use self::block::{Block, Blockish};
use self::player::Player;
pub struct Level {
dimensions: (u32, u32),
move_stack: VecDeque<()>,
blocks: Vec<Block>,
player1: Player,
player2: Player,
goal1: (i32, i32),
goal2: (i32, i32),
}
#[derive(Copy, Clone, Debug, PartialOrd, PartialEq)]
pub struct Segment {
position: (i32, i32),
shape: Shape,
board: Board,
}
#[derive(Copy, Clone, Debug, Hash, PartialEq, Eq)]
pub enum Entity {
Block(usize),
Player(Board),
}
pub type ChangeSet = HashMap<Entity, PushDir>;
pub type FailSet = HashSet<usize>;
impl Level {
pub fn from_json(data: impl AsRef<str>) -> Level {
let data: LevelData = json5::from_str(data.as_ref()).unwrap();
println!("level data: {:?}", data);
let blocks = data
.blocks
.iter()
.enumerate()
.map(|(i, block)| Block::from_data(i, block))
.collect();
let player1 = Player {
position: data.player1.position,
color: data.player1.color.into(),
};
let player2 = Player {
position: data.player2.position,
color: data.player2.color.into(),
};
Level {
dimensions: (data.dimensions[0], data.dimensions[1]),
move_stack: VecDeque::new(),
blocks,
player1,
player2,
goal1: data.goal1,
goal2: data.goal2,
}
}
// check if we won
pub fn check_win_condition(&self) -> bool {
self.player1.position == self.goal1 && self.player2.position == self.goal2
}
pub fn apply_change_set(&mut self, change_set: ChangeSet) {
for (entity, direction) in change_set {
let direction = direction.as_pair();
match entity {
Entity::Player(board) => {
let player = match board {
Board::Left => &mut self.player1,
Board::Right => &mut self.player2,
};
player.position.0 += direction.0;
player.position.1 += direction.1;
}
Entity::Block(index) => {
let block = self.blocks.get_mut(index).expect("big failure");
for segment in &mut block.segments {
segment.position.0 += direction.0;
segment.position.1 += direction.1;
}
}
}
}
}
pub fn try_move(&mut self, board: Board, direction: PushDir) -> Result<ChangeSet, FailSet> {
let mut change_set = ChangeSet::default();
change_set.insert(Entity::Player(board), direction);
self.player_can_move(board, direction, change_set)
}
fn player_can_move(
&self,
board: Board,
direction: PushDir,
change_set: ChangeSet,
) -> Result<ChangeSet, FailSet> {
let player = match board {
Board::Left => &self.player1,
Board::Right => &self.player2,
};
let player_segment = Segment {
position: player.position,
shape: Shape::Full,
board,
};
self.segment_can_move(None, player_segment, direction, change_set)
}
fn block_can_move(
&self,
index: usize,
direction: PushDir,
mut change_set: ChangeSet,
) -> Result<ChangeSet, FailSet> {
println!("block_can_move({:?}, {:?})", index, direction);
let block = match self.blocks.get(index) {
Some(block) => block,
None => return Err(HashSet::new()),
};
// is the block even movable?
if !block.movable {
return Err(set!(index));
}
// does the direction match the orientation?
match (block.orientation, direction) {
(Orientation::Horizontal, PushDir::Left)
| (Orientation::Horizontal, PushDir::Right)
| (Orientation::Vertical, PushDir::Up)
| (Orientation::Vertical, PushDir::Down)
| (Orientation::Both, _) => (),
_ => return Err(set!(index)),
}
// TODO: change this to use &mut instead of returning a new one each time
change_set.insert(Entity::Block(index), direction);
for segment in block.get_segments() {
match self.segment_can_move(Some(index), segment, direction, change_set.clone()) {
Ok(new_change_set) => change_set = new_change_set,
Err(fail_set) => return Err(fail_set),
}
}
Ok(change_set)
}
fn segment_can_move(
&self,
block_index: Option<usize>,
segment: Segment,
direction: PushDir,
change_set: ChangeSet,
) -> Result<ChangeSet, FailSet> {
println!(
"segment_can_move({:?}, {:?}, {:?})",
block_index, segment, direction
);
let segment_loc = (segment.position.0, segment.position.1, segment.board);
let target = segment_loc + direction;
println!(" - target: {:?}", target);
// is the target actually in the map?
if target.0 < 0
|| target.0 >= self.dimensions.0 as i32
|| target.1 < 0
|| target.1 >= self.dimensions.1 as i32
{
return Err(entity_fail!(block_index));
}
// retrieve other blocks that might be occupying this current space and the target space
let mut current_occupant = None;
let mut target_occupant = None;
for (i, block) in self.blocks.iter().enumerate() {
// skip other segments of the same block
if let Some(n) = block_index {
if n == i {
continue;
}
}
// offset from the change set
let offset = match change_set.get(&Entity::Block(i)) {
Some(direction) => direction.as_pair(),
None => (0, 0),
};
for segment in block.get_segments() {
// don't get segments on different boards
if segment.board != segment_loc.2 {
continue;
}
let mut segment_pos = segment.position;
if segment_pos == (segment_loc.0, segment_loc.1) {
current_occupant = Some((i, segment.shape, block.orientation));
}
segment_pos.0 += offset.0;
segment_pos.1 += offset.1;
if segment_pos == (target.0, target.1) {
target_occupant = Some((Entity::Block(i), segment.shape, block.orientation));
}
}
}
// check if the target occupant is actually a player
if let None = target_occupant {
if segment.board == Board::Left && self.player1.position == (target.0, target.1) {
target_occupant =
Some((Entity::Player(Board::Left), Shape::Full, Orientation::None));
} else if segment.board == Board::Right && self.player2.position == (target.0, target.1)
{
target_occupant =
Some((Entity::Player(Board::Right), Shape::Full, Orientation::None));
}
}
println!(
" - occupants: current={:?} | target={:?}",
current_occupant, target_occupant
);
// handle special pushes
if let Some((other_block, other_shape, other_orientation)) = current_occupant {
// are both shapes triangles?
let both_triangles = match (segment.shape, other_shape) {
(Shape::Full, Shape::Full) => false,
(Shape::Full, _) => unreachable!("invalid to have triangle + full"),
(_, Shape::Full) => unreachable!("invalid to have triangle + full"),
_ => true,
};
if both_triangles {
// what directions could we be pushing the other block into?
let possible_directions = match segment.shape {
Shape::TopRight => [PushDir::Up, PushDir::Right],
Shape::TopLeft => [PushDir::Left, PushDir::Up],
Shape::BottomLeft => [PushDir::Down, PushDir::Left],
Shape::BottomRight => [PushDir::Right, PushDir::Down],
Shape::Full => unreachable!("already eliminated this possibility"),
};
println!(" - possible directions: {:?}", possible_directions);
// does the direction we're pushing appear in this list?
if possible_directions.contains(&direction) {
// the other shape goes in the other direction
let other_direction = match other_orientation {
Orientation::None => unreachable!("already eliminated this possibility"),
Orientation::Vertical => [PushDir::Up, PushDir::Down],
Orientation::Horizontal => [PushDir::Left, PushDir::Right],
Orientation::Both => unimplemented!(),
};
let possible_directions = possible_directions.iter().collect::<HashSet<_>>();
let other_direction = other_direction.iter().collect();
let mut intersected_direction =
possible_directions.intersection(&other_direction);
let new_direction = **intersected_direction.next().unwrap();
// let other_direction = {
// let mut set = possible_directions.iter().collect::<HashSet<_>>();
// set.remove(&direction);
// *set.into_iter().next().unwrap()
// };
let mut result = self.block_can_move(other_block, new_direction, change_set);
if let Ok(ref mut change_set) = result {
change_set.insert(Entity::Block(other_block), new_direction);
}
return result;
}
}
}
// handle normal pushes
if let Some((entity, shape, _orientation)) = target_occupant {
match entity {
Entity::Player(_) => {
// TODO: assert that the board is the same
Err(fail_set!(change_set))
}
Entity::Block(index) => {
if
// if it's part of the same block it's ok to push
block_index.is_some() && block_index.unwrap() == index ||
// if the shapes are opposite, we can actually both fit into the same spot
segment.shape.is_opposite(shape)
{
Ok(change_set)
}
// if the block is already in the change set, it can't move
else if change_set.contains_key(&Entity::Block(index)) {
Err(fail_set!(change_set))
}
// if the next block can move then so can this one
else {
self.block_can_move(index, direction, change_set)
}
}
}
} else {
// coast is clear, push away!
Ok(change_set)
}
}
pub fn render(&self, renderer: &mut Renderer, animations: &AnimationState) {
// board positioning calculations
let playfield_ratio = (2 * self.dimensions.0 + 6) as f32 / (self.dimensions.1 + 4) as f32;
let screen_ratio = renderer.window.0 / renderer.window.1;
let cols = self.dimensions.0 as i32;
let rows = self.dimensions.1 as i32;
let (scale, xoff, yoff) = if playfield_ratio > screen_ratio {
let scale = renderer.window.0 as i32 / (2 * cols + 6);
let yoff = renderer.window.1 as i32 / 2 - (rows + 4) * scale / 2;
(scale, 0, yoff)
} else {
let scale = renderer.window.1 as i32 / (rows + 4);
let xoff = renderer.window.0 as i32 / 2 - (2 * cols + 6) * scale / 2;
(scale, xoff, 0)
};
self.render_boards(renderer, scale, (xoff, yoff), animations);
}
fn render_boards(
&self,
renderer: &mut Renderer,
scale: i32,
offset: (i32, i32),
animations: &AnimationState,
) {
let left_off = (offset.0 + 2 * scale, offset.1 + 2 * scale);
let right_off = (
offset.0 + (4 + self.dimensions.0 as i32) * scale,
offset.1 + 2 * scale,
);
// render the grid
// TODO: do this in one single pass instead of once for each cell
for x in 0..self.dimensions.0 as i32 {
for y in 0..self.dimensions.1 as i32 {
renderer.render_cell((left_off.0 + x * scale, left_off.1 + y * scale), scale);
renderer.render_cell((right_off.0 + x * scale, right_off.1 + y * scale), scale);
}
}
// render blocks
for (i, block) in self.blocks.iter().enumerate() {
for segment in block.get_segments().iter() {
let offset = match &segment.board {
Board::Left => left_off,
Board::Right => right_off,
};
let mut location = (
offset.0 + segment.position.0 * scale,
offset.1 + segment.position.1 * scale,
);
let animation_offset = animations.get_block_offset(i);
location.0 += (animation_offset.0 * scale as f32) as i32;
location.1 += (animation_offset.1 * scale as f32) as i32;
renderer.render_segment(
location,
scale,
block.get_color(),
block.get_orientation(),
segment.shape,
);
}
}
// render goals
self.render_goal(renderer, self.goal1, scale, left_off);
self.render_goal(renderer, self.goal2, scale, right_off);
// render player
self.render_player(
renderer,
Board::Left,
&self.player1,
scale,
animations,
left_off,
);
self.render_player(
renderer,
Board::Right,
&self.player2,
scale,
animations,
right_off,
);
}
fn render_player(
&self,
renderer: &mut Renderer,
board: Board,
player: &Player,
scale: i32,
animations: &AnimationState,
offset: (i32, i32),
) {
let mut location = (
offset.0 + player.position.0 * scale + 4,
offset.1 + player.position.1 * scale + 4,
);
let animation_offset = animations.get_player_offset(board);
location.0 += (animation_offset.0 * scale as f32) as i32;
location.1 += (animation_offset.1 * scale as f32) as i32;
renderer.render_segment(
location,
scale - 8,
player.color,
Orientation::Both,
Shape::Full,
);
}
fn render_goal(
&self,
renderer: &mut Renderer,
location: (i32, i32),
scale: i32,
offset: (i32, i32),
) {
let position = (
offset.0 + location.0 * scale + 4,
offset.1 + location.1 * scale + 4,
);
renderer.render_segment(
position,
scale - 8,
Color::from_rgb_u32(102, 204, 102),
Orientation::Both,
Shape::Full,
);
}
}

23
src/level/player.rs Normal file
View file

@ -0,0 +1,23 @@
use crate::color::Color;
use crate::enums::Orientation;
use crate::level::{Blockish, Segment};
#[derive(Copy, Clone)]
pub struct Player {
pub position: (i32, i32),
pub color: Color,
}
impl Blockish for Player {
fn get_color(&self) -> Color {
self.color
}
fn get_orientation(&self) -> Orientation {
Orientation::None
}
fn get_segments(&self) -> Vec<Segment> {
vec![]
}
}

View file

@ -1,30 +0,0 @@
macro_rules! set {
($($item:expr)*) => {
{
let mut set = std::collections::HashSet::new();
$(set.insert($item);)*
set
}
}
}
macro_rules! fail_set {
($change_set:expr) => {
$change_set
.iter()
.filter_map(|(entity, _)| match entity {
Entity::Block(index) => Some(*index),
Entity::Player(_) => None,
})
.collect()
};
}
macro_rules! entity_fail {
($item:expr) => {
match $item {
Some(index) => set!(index),
None => std::collections::HashSet::new(),
}
};
}

View file

@ -1,46 +1,75 @@
#[macro_use] #[macro_use]
extern crate serde; extern crate glium;
extern crate nalgebra_glm as glm;
#[macro_use] #[macro_use]
pub mod macros; extern crate serde_derive;
pub mod animations; mod animations;
pub mod draw; mod color;
pub mod game_state; mod data;
pub mod screens; mod enums;
mod game;
mod keymap;
mod level;
mod renderer;
mod resources;
mod screens;
use anyhow::Result; use std::time::Instant;
use macroquad::{
prelude::*,
ui::{hash, root_ui, widgets::Window},
};
use screens::{Screen, ScreenAction};
use crate::screens::{MenuScreen, ScreenStack}; use glium::glutin::dpi::PhysicalSize;
use glium::glutin::{ContextBuilder, Event, EventsLoop, WindowBuilder, WindowEvent};
use glium::{Display, Surface};
#[macroquad::main("BasicShapes")] use crate::game::Game;
async fn main() -> Result<()> {
let mut screen_stack = ScreenStack::with(MenuScreen::new());
loop { const GAME_WIDTH: u32 = 1024;
clear_background(WHITE); const GAME_HEIGHT: u32 = 768;
Window::new(hash!(), vec2(20., 20.), vec2(120., 120.)) fn main() {
.titlebar(true) let mut events_loop = EventsLoop::new();
.label("level control") let primary_monitor = events_loop.get_primary_monitor();
.ui(&mut root_ui(), |ui| { let dpi_factor = primary_monitor.get_hidpi_factor();
ui.button(vec2(0., 0.), "helloge"); let dimensions: PhysicalSize = (GAME_WIDTH, GAME_HEIGHT).into();
let top = screen_stack.top(); let wb = WindowBuilder::new()
let top = top.as_ref(); .with_dimensions(dimensions.to_logical(dpi_factor))
ui.label(vec2(0.0, 10.0), &format!("screen: {}", top.status())); .with_resizable(false)
.with_title("wedge");
let cb = ContextBuilder::new();
let display = Display::new(wb, cb, &events_loop).unwrap();
{
let gl_window = display.gl_window();
let window = gl_window.window();
println!("size: {:?}", window.get_inner_size());
}
let mut game = Game::new(&display);
let mut closed = false;
let mut prev = Instant::now();
while !closed {
let now = Instant::now();
let delta = now - prev;
events_loop.poll_events(|event| match event {
Event::WindowEvent {
event: WindowEvent::CloseRequested,
..
} => closed = true,
_ => game.handle_event(event),
}); });
screen_stack.render(); game.update(delta);
screen_stack.update();
next_frame().await let mut target = display.draw();
} target.clear(None, Some((0.0, 0.0, 0.0, 1.0)), true, None, None);
let mut renderer = game.create_renderer(&mut target);
game.render(&mut renderer);
target.finish().unwrap();
Ok(()) prev = now;
std::thread::sleep(std::time::Duration::from_millis(17));
}
} }

229
src/renderer.rs Normal file
View file

@ -0,0 +1,229 @@
use glium::draw_parameters::{Blend, DrawParameters};
use glium::index::{NoIndices, PrimitiveType};
use glium::{Display, Frame, Program, Surface, Texture2d, VertexBuffer};
use nalgebra::{Matrix4, Vector4};
use crate::color::Color;
use crate::enums::{Orientation, Shape};
use crate::game::Game;
pub struct Renderer<'a, 'b> {
pub window: (f32, f32),
target: &'a mut Frame,
display: &'b Display,
cell_program: &'b Program,
segment_program: &'b Program,
segment_texture: &'b Texture2d,
}
impl<'a, 'b> Renderer<'a, 'b> {
pub fn new(game: &'b Game, target: &'a mut Frame) -> Self {
Renderer {
window: (
game.resources.window_dimensions.0 as f32,
game.resources.window_dimensions.1 as f32,
),
target,
display: &game.display,
cell_program: game.resources.get_shader("cell").unwrap(),
segment_program: game.resources.get_shader("segment").unwrap(),
segment_texture: game.resources.get_texture("segment").unwrap(),
}
}
pub fn render_cell(&mut self, location: (i32, i32), scale: i32) {
#[derive(Copy, Clone)]
struct Vertex {
point: [f32; 2],
}
implement_vertex!(Vertex, point);
let indices = NoIndices(PrimitiveType::TrianglesList);
let mut vertices = Vec::<Vertex>::new();
vertices.push(Vertex { point: [0.0, 0.0] });
vertices.push(Vertex { point: [1.0, 0.0] });
vertices.push(Vertex { point: [0.0, 1.0] });
vertices.push(Vertex { point: [1.0, 1.0] });
vertices.push(Vertex { point: [0.0, 1.0] });
vertices.push(Vertex { point: [1.0, 0.0] });
let vertex_buffer = VertexBuffer::new(self.display, &vertices).unwrap();
let projection = glm::ortho::<f32>(
0.0,
self.window.0 as f32,
self.window.1 as f32,
0.0,
-1.0,
1.0,
);
let mut matrix = Matrix4::<f32>::identity();
matrix = matrix.append_nonuniform_scaling(&[scale as f32, scale as f32, 1.0].into());
matrix = matrix.append_translation(&[location.0 as f32, location.1 as f32, 0.0].into());
let color = Vector4::from([0.6, 0.6, 0.8, 1.0f32]);
let uniforms = uniform! {
target: *matrix.as_ref(),
projection: *projection.as_ref(),
color: *color.as_ref(),
};
self.target
.draw(
&vertex_buffer,
&indices,
&self.cell_program,
&uniforms,
&DrawParameters {
blend: Blend::alpha_blending(),
..Default::default()
},
)
.unwrap();
}
pub fn render_segment(
&mut self,
location: (i32, i32),
scale: i32,
color: Color,
orientation: Orientation,
shape: Shape,
) {
#[derive(Copy, Clone)]
struct Vertex {
pos: [f32; 2],
tex: [f32; 2],
}
implement_vertex!(Vertex, pos, tex);
let indices = NoIndices(PrimitiveType::TrianglesList);
let mut vertices = Vec::new();
match shape {
Shape::BottomLeft => {
vertices.push(Vertex {
pos: [1.0, 1.0],
tex: [1.0, 1.0],
});
vertices.push(Vertex {
pos: [1.0, 0.0],
tex: [1.0, 0.0],
});
vertices.push(Vertex {
pos: [0.0, 0.0],
tex: [0.0, 0.0],
});
}
Shape::TopLeft => {
vertices.push(Vertex {
pos: [0.0, 1.0],
tex: [0.0, 1.0],
});
vertices.push(Vertex {
pos: [1.0, 1.0],
tex: [1.0, 1.0],
});
vertices.push(Vertex {
pos: [1.0, 0.0],
tex: [1.0, 0.0],
});
}
Shape::TopRight => {
vertices.push(Vertex {
pos: [1.0, 1.0],
tex: [1.0, 1.0],
});
vertices.push(Vertex {
pos: [0.0, 1.0],
tex: [0.0, 1.0],
});
vertices.push(Vertex {
pos: [0.0, 0.0],
tex: [0.0, 0.0],
});
}
Shape::BottomRight => {
vertices.push(Vertex {
pos: [0.0, 1.0],
tex: [0.0, 1.0],
});
vertices.push(Vertex {
pos: [1.0, 0.0],
tex: [1.0, 0.0],
});
vertices.push(Vertex {
pos: [0.0, 0.0],
tex: [0.0, 0.0],
});
}
_ => {
vertices.push(Vertex {
pos: [0.0, 1.0],
tex: [0.0, 1.0],
});
vertices.push(Vertex {
pos: [1.0, 0.0],
tex: [1.0, 0.0],
});
vertices.push(Vertex {
pos: [0.0, 0.0],
tex: [0.0, 0.0],
});
vertices.push(Vertex {
pos: [0.0, 1.0],
tex: [0.0, 1.0],
});
vertices.push(Vertex {
pos: [1.0, 1.0],
tex: [1.0, 1.0],
});
vertices.push(Vertex {
pos: [1.0, 0.0],
tex: [1.0, 0.0],
});
}
}
let vertex_buffer = VertexBuffer::new(self.display, &vertices).unwrap();
let tint = Vector4::from([color.0, color.1, color.2, 1.0f32]);
let projection = glm::ortho::<f32>(
0.0,
self.window.0 as f32,
self.window.1 as f32,
0.0,
-1.0,
1.0,
);
let mut matrix = Matrix4::<f32>::identity();
matrix = matrix.append_nonuniform_scaling(&[scale as f32, scale as f32, 1.0].into());
matrix = matrix.append_translation(&[location.0 as f32, location.1 as f32, 0.0].into());
let rotate_texture = match orientation {
Orientation::Both => false,
Orientation::None => false,
Orientation::Vertical => true,
Orientation::Horizontal => false,
};
let uniforms = uniform! {
target: *matrix.as_ref(),
rotate_texture: rotate_texture,
projection: *projection.as_ref(),
tint: *tint.as_ref(),
tex: self.segment_texture,
};
self.target
.draw(
&vertex_buffer,
&indices,
&self.segment_program,
&uniforms,
&DrawParameters {
blend: Blend::alpha_blending(),
..Default::default()
},
)
.unwrap();
}
}

66
src/resources.rs Normal file
View file

@ -0,0 +1,66 @@
use std::collections::HashMap;
use glium::texture::RawImage2d;
use glium::{Display, Program, ProgramCreationError, Texture2d};
use image::{DynamicImage, GenericImageView, ImageError};
#[derive(Default)]
pub struct Resources {
pub window_dimensions: (u32, u32),
textures: HashMap<String, Texture2d>,
shaders: HashMap<String, Program>,
}
impl Resources {
pub fn load_image_from_memory(
&mut self,
display: &Display,
name: impl AsRef<str>,
buffer: &[u8],
alpha: bool,
) -> Result<(), ImageError> {
let image = image::load_from_memory(buffer)?;
self.load_image(display, name, image, alpha);
Ok(())
}
pub fn load_image(
&mut self,
display: &Display,
name: impl AsRef<str>,
image: DynamicImage,
alpha: bool,
) {
let name = name.as_ref().to_owned();
let dimensions = image.dimensions();
let image = if alpha {
RawImage2d::from_raw_rgba_reversed(&image.raw_pixels(), dimensions)
} else {
RawImage2d::from_raw_rgb_reversed(&image.raw_pixels(), dimensions)
};
// TODO: don't unwrap
let texture = Texture2d::new(display, image).unwrap();
self.textures.insert(name, texture);
}
pub fn get_texture(&self, name: impl AsRef<str>) -> Option<&Texture2d> {
self.textures.get(name.as_ref())
}
pub fn load_shader(
&mut self,
display: &Display,
name: impl AsRef<str>,
vertex: &str,
fragment: &str,
) -> Result<(), ProgramCreationError> {
let name = name.as_ref().to_owned();
let program = Program::from_source(display, vertex, fragment, None)?;
self.shaders.insert(name, program);
Ok(())
}
pub fn get_shader(&self, name: impl AsRef<str>) -> Option<&Program> {
self.shaders.get(name.as_ref())
}
}

View file

@ -1,17 +1,15 @@
use macroquad::{input::is_key_pressed, miniquad::KeyCode}; use std::time::Duration;
// use crate::keymap::Keymap; use glium::glutin::VirtualKeyCode;
use crate::keymap::Keymap;
use crate::screens::{PlayScreen, Screen, ScreenAction}; use crate::screens::{PlayScreen, Screen, ScreenAction};
pub struct MenuScreen; pub struct MenuScreen;
impl Screen for MenuScreen { impl Screen for MenuScreen {
fn status(&self) -> String { fn update(&mut self, delta: Duration, keymap: &Keymap) -> ScreenAction {
format!("menu screen") if keymap.is_pressed(VirtualKeyCode::Space) {
}
fn update(&mut self) -> ScreenAction {
if is_key_pressed(KeyCode::Space) {
let play_screen = PlayScreen::new(); let play_screen = PlayScreen::new();
ScreenAction::Push(Box::new(play_screen)) ScreenAction::Push(Box::new(play_screen))
} else { } else {

View file

@ -1,17 +1,20 @@
mod menu; mod menu;
mod play; mod play;
use std::time::Duration;
use crate::keymap::Keymap;
use crate::renderer::Renderer;
pub use self::menu::MenuScreen; pub use self::menu::MenuScreen;
pub use self::play::PlayScreen; pub use self::play::PlayScreen;
pub trait Screen { pub trait Screen {
fn status(&self) -> String; fn update(&mut self, delta: Duration, keymap: &Keymap) -> ScreenAction {
fn update(&mut self) -> ScreenAction {
ScreenAction::None ScreenAction::None
} }
fn render(&self) {} fn render(&self, renderer: &mut Renderer) {}
} }
pub enum ScreenAction { pub enum ScreenAction {
@ -22,7 +25,7 @@ pub enum ScreenAction {
pub struct ScreenStack(Vec<Box<dyn Screen>>); pub struct ScreenStack(Vec<Box<dyn Screen>>);
impl ScreenStack { impl ScreenStack {
pub fn with<S: Screen + 'static>(screen: S) -> Self { pub fn with<S: 'static + Screen>(screen: S) -> Self {
let mut stack = Vec::<Box<dyn Screen>>::new(); let mut stack = Vec::<Box<dyn Screen>>::new();
stack.push(Box::new(screen)); stack.push(Box::new(screen));
ScreenStack(stack) ScreenStack(stack)
@ -36,11 +39,11 @@ impl ScreenStack {
self.0.last_mut().unwrap() self.0.last_mut().unwrap()
} }
pub fn update(&mut self) { pub fn update(&mut self, delta: Duration, keymap: &Keymap) {
let result = { let result = {
let mut screen = self.top_mut(); let mut screen = self.top_mut();
let screen = screen.as_mut(); let screen = screen.as_mut();
screen.update() screen.update(delta, keymap)
}; };
match result { match result {
ScreenAction::None => (), ScreenAction::None => (),
@ -51,9 +54,9 @@ impl ScreenStack {
} }
} }
pub fn render(&self) { pub fn render(&self, renderer: &mut Renderer) {
let screen = self.top(); let screen = self.top();
let screen = screen.as_ref(); let screen = screen.as_ref();
screen.render() screen.render(renderer)
} }
} }

View file

@ -1,14 +1,13 @@
use std::time::Duration; use std::time::Duration;
use macroquad::{ use glium::glutin::VirtualKeyCode;
input::is_key_pressed, miniquad::KeyCode, time::get_frame_time,
};
use crate::{ use crate::animations::AnimationState;
animations::AnimationState, use crate::enums::{Board, PushDir};
game_state::{Board, LevelData, LevelState, PushDir}, use crate::keymap::Keymap;
screens::{Screen, ScreenAction}, use crate::level::Level;
}; use crate::renderer::Renderer;
use crate::screens::{Screen, ScreenAction};
const LEVEL_TUTORIAL: &str = include_str!("../../levels/tutorial.json"); const LEVEL_TUTORIAL: &str = include_str!("../../levels/tutorial.json");
const LEVEL_TUTORIAL2: &str = include_str!("../../levels/tutorial2.json"); const LEVEL_TUTORIAL2: &str = include_str!("../../levels/tutorial2.json");
@ -17,19 +16,15 @@ const LEVEL_1: &str = include_str!("../../levels/level1.json");
pub struct PlayScreen { pub struct PlayScreen {
animations: AnimationState, animations: AnimationState,
levels: Vec<&'static str>, levels: Vec<&'static str>,
current_level: LevelState, current_level: Level,
current_level_idx: usize, current_level_idx: usize,
} }
impl Screen for PlayScreen { impl Screen for PlayScreen {
fn status(&self) -> String { fn update(&mut self, delta: Duration, keymap: &Keymap) -> ScreenAction {
format!("play screen")
}
fn update(&mut self) -> ScreenAction {
macro_rules! btn_handler { macro_rules! btn_handler {
($key:expr, $board:expr, $direction:expr) => { ($key:expr, $board:expr, $direction:expr) => {
if is_key_pressed($key) { if keymap.is_pressed($key) {
println!("pushed: {:?}", $key); println!("pushed: {:?}", $key);
let level = self.get_current_level_mut(); let level = self.get_current_level_mut();
let result = level.try_move($board, $direction); let result = level.try_move($board, $direction);
@ -39,15 +34,14 @@ impl Screen for PlayScreen {
} }
if self.animations.is_animating { if self.animations.is_animating {
let delta = Duration::from_secs_f32(get_frame_time()); // println!("animating. {:?}", self.animations.progress);
self.animations.make_progress(delta); self.animations.make_progress(delta);
// we just finished! // we just finished!
if !self.animations.is_animating { if !self.animations.is_animating {
// apply the changes to the entities // apply the changes to the entities
// this indirection is used to dodge a concurrent borrow // this indirection is used to dodge a concurrent borrow
let change_set = let change_set = if let Some(Ok(change_set)) = &self.animations.last_move_result {
if let Some(Ok(change_set)) = &self.animations.last_move_result {
Some(change_set.clone()) Some(change_set.clone())
} else { } else {
None None
@ -59,17 +53,17 @@ impl Screen for PlayScreen {
} }
} }
} else { } else {
btn_handler!(KeyCode::W, Board::Left, PushDir::Up); btn_handler!(VirtualKeyCode::W, Board::Left, PushDir::Up);
btn_handler!(KeyCode::A, Board::Left, PushDir::Left); btn_handler!(VirtualKeyCode::A, Board::Left, PushDir::Left);
btn_handler!(KeyCode::S, Board::Left, PushDir::Down); btn_handler!(VirtualKeyCode::S, Board::Left, PushDir::Down);
btn_handler!(KeyCode::D, Board::Left, PushDir::Right); btn_handler!(VirtualKeyCode::D, Board::Left, PushDir::Right);
btn_handler!(KeyCode::I, Board::Right, PushDir::Up); btn_handler!(VirtualKeyCode::I, Board::Right, PushDir::Up);
btn_handler!(KeyCode::J, Board::Right, PushDir::Left); btn_handler!(VirtualKeyCode::J, Board::Right, PushDir::Left);
btn_handler!(KeyCode::K, Board::Right, PushDir::Down); btn_handler!(VirtualKeyCode::K, Board::Right, PushDir::Down);
btn_handler!(KeyCode::L, Board::Right, PushDir::Right); btn_handler!(VirtualKeyCode::L, Board::Right, PushDir::Right);
if is_key_pressed(KeyCode::R) { if keymap.is_pressed(VirtualKeyCode::R) {
// restart the level // restart the level
self.restart_level(); self.restart_level();
} }
@ -78,29 +72,31 @@ impl Screen for PlayScreen {
ScreenAction::None ScreenAction::None
} }
fn render(&self) { fn render(&self, renderer: &mut Renderer) {
let level = self.get_current_level(); let level = self.get_current_level();
level.render(&self.animations); level.render(renderer, &self.animations);
} }
} }
impl PlayScreen { impl PlayScreen {
pub fn get_current_level(&self) -> &LevelState { pub fn get_current_level(&self) -> &Level {
&self.current_level &self.current_level
} }
pub fn get_current_level_mut(&mut self) -> &mut LevelState { pub fn get_current_level_mut(&mut self) -> &mut Level {
&mut self.current_level &mut self.current_level
} }
pub fn new() -> PlayScreen { pub fn new() -> PlayScreen {
let levels = vec![LEVEL_TUTORIAL, LEVEL_TUTORIAL2, LEVEL_1]; let levels = vec![
LEVEL_TUTORIAL,
LEVEL_TUTORIAL2,
LEVEL_1,
];
PlayScreen { PlayScreen {
levels, levels,
current_level: LevelState::new( current_level: Level::from_json(&LEVEL_TUTORIAL),
LevelData::load_from_string(&LEVEL_TUTORIAL).unwrap(),
),
current_level_idx: 0, current_level_idx: 0,
animations: AnimationState::new(), animations: AnimationState::new(),
} }
@ -111,8 +107,7 @@ impl PlayScreen {
} }
fn switch_to_level(&mut self, idx: usize) { fn switch_to_level(&mut self, idx: usize) {
self.current_level = self.current_level = Level::from_json(self.levels[idx]);
LevelState::new(LevelData::load_from_string(&self.levels[idx]).unwrap());
self.current_level_idx = idx; self.current_level_idx = idx;
} }

BIN
textures/segment.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB