Browse Source

GameBridge->Bridge, Game->Harbour, State->Game

master
Inderjit Gill 1 year ago
parent
commit
19e27025d5
5 changed files with 783 additions and 779 deletions
  1. +629
    -63
      src/game.rs
  2. +107
    -0
      src/harbour.rs
  3. +28
    -26
      src/lib.rs
  4. +0
    -671
      src/state.rs
  5. +19
    -19
      web/js/index.js

+ 629
- 63
src/game.rs View File

@@ -1,105 +1,671 @@
use log;
use controller::{Controller, ControllerButton, ControllerAction};
use state::State;
use std::collections::HashMap;
use units::*;
use sprite::Sprite;
use controller::Controller;
use geometry::Geometry;

#[derive(PartialEq)]
pub enum GameMode {
Playing,
Paused,
#[derive(Clone, Copy)]
pub struct Block {
active: bool,
offset: Vec2D
}

impl Block {
pub fn new() -> Block {
Block {
active: false,
offset: Vec2D {x: 0.0, y: 0.0 },
}
}
}

pub struct Board {
board: Vec<Vec<Block>>,
width: usize,
height: usize,
}

impl Board {
pub fn new(width: usize, height: usize) -> Board {
Board {
board: vec![vec![Block::new(); width]; height],
width,
height,
}
}
}

#[derive(Debug, Clone, Copy)]
pub struct BoardPos {
pub x: i32,
pub y: i32,
}

#[derive(Clone, Copy, Hash, Eq, PartialEq)]
enum ShapeKind {
I,
J,
L,
O,
S,
T,
Z,
}

pub struct Shape {
aabb_width: i32,
aabb_height: i32,
real_height: i32,
fully_rotate: bool,
form: Vec<Vec<bool>>,
}

enum PieceAngle {
R0,
R90,
R180,
R270,
}

pub struct Piece {
shape_kind: ShapeKind,
pos: BoardPos,
angle: PieceAngle,
}

pub struct Game {
delta: f32,
board: Board,
board_offset: Block2D,

shapes: HashMap<ShapeKind, Shape>,
piece: Option<Piece>, // the current piece under player control
next_shape: ShapeKind,

score: i32,
level: i32,
lines: i32,

mode: GameMode,
resume_from_paused: bool,

controller: Controller,
state: State,
geometry: Geometry,
time_to_next_lowering: f32, // ms
piece_is_dropping: bool,

default_cooldown: f32, // ms
left_cooldown: f32,
right_cooldown: f32,
up_cooldown: f32,
down_cooldown: f32,
a_cooldown: f32,
b_cooldown: f32,

terrible_rng: i32,
}

impl Game {
pub fn new(
canvas_width: i32,
canvas_height: i32,
block_size: i32,
tileset_texture_width: i32, // tileset texture width
tileset_texture_height: i32, // tileset texture height
) -> Game {
pub fn new() -> Game {
Game {
delta: 0.0,
board: Board::new(10, 20),
board_offset: Block2D { x: 5, y: 5 },

shapes: define_shapes(),
piece: Some(Piece {
shape_kind: ShapeKind::J,
pos: BoardPos {
x: 5,
y: 19,
},
angle: PieceAngle::R0,
}),
next_shape: ShapeKind::L,

mode: GameMode::Playing,
resume_from_paused: false,
score: 0,
level: 0,
lines: 0,

controller: Controller::new(),
time_to_next_lowering: get_time_to_next_lowering(0),
piece_is_dropping: false,

state: State::new(),
default_cooldown: 100.0,
left_cooldown: 0.0,
right_cooldown: 0.0,
up_cooldown: 0.0,
down_cooldown: 0.0,
a_cooldown: 0.0,
b_cooldown: 0.0,

geometry: Geometry::new(canvas_width,
canvas_height,
block_size,
tileset_texture_width,
tileset_texture_height),
terrible_rng: 0
}
}

pub fn init(&mut self, random: f32) {
self.state.init(random);
self.terrible_rng = (random * 100.0) as i32;
self.next_shape = get_random_shape(self);
self.terrible_rng = (random * 140.0) as i32;
use_next_shape(self);
}

// return true if we need to call tick after receiving an input event
// (only true when we resume playing after being paused)
pub fn input(&mut self, button: ControllerButton, action: ControllerAction) -> bool {
self.controller.input(button, action);
pub fn update_geometry(&self, geometry: &mut Geometry) {
log("game: update_geometry");

if button == ControllerButton::Start && action == ControllerAction::Up {
if self.mode == GameMode::Playing {
log("Paused");
self.mode = GameMode::Paused;
} else {
log("Playing");
self.mode = GameMode::Playing;
self.resume_from_paused = true;
return true;
geometry.clear();

// zero vert
geometry.push(0.0, 0.0, 1.0, 0.0, 1.0, 1.0, 0.0, 0.0);
render_board(geometry, &self.board.board, &self.board_offset);
render_score(geometry, self.score, Block2D { x: 19, y: 24 }, Col::new(1.0, 0.2, 0.3, 1.0));
render_level(geometry, self.level, Block2D { x: 19, y: 20 }, Col::new(1.0, 0.2, 0.3, 1.0));
render_lines(geometry, self.lines, Block2D { x: 19, y: 16 }, Col::new(1.0, 0.2, 0.3, 1.0));

if let Some(shape) = self.shapes.get(&self.next_shape) {
render_next_shape(geometry, &shape, Block2D { x: 19, y: 10 }, Col::new(1.0, 0.2, 0.3, 1.0));
}

}

pub fn update_geometry_debug(&self, _geometry: &mut Geometry) {

}

// counting on the fact that delta will always be a 'sensible' value.
// so if the player has paused the game for a while, on the first resumed call to tick_playing
// delta should be 0 rather than a large multi-second value
pub fn tick_playing(&mut self, controller: &Controller, delta: f32, elapsed: f32) -> bool {

self.terrible_rng = elapsed as i32;

if let Some(ref piece) = self.piece {
if let Some(shape) = self.shapes.get(&piece.shape_kind) {
remove_shape_from_board(&mut self.board, shape, &piece.pos, &piece.angle);
}
}

apply_user_input(self, controller, delta);

if self.piece_is_dropping {
if let Some(ref mut piece) = self.piece {
if let Some(shape) = self.shapes.get(&piece.shape_kind) {
let has_lowered = lower_piece(&self.board, shape, piece);
if !has_lowered {
self.piece_is_dropping = false;
}
}
}
} else if apply_lowering(self, delta) {
use_next_shape(self);
}

if let Some(ref piece) = self.piece {
if let Some(shape) = self.shapes.get(&piece.shape_kind) {
add_shape_to_board(&mut self.board, shape, &piece.pos, &piece.angle);
}
}

true
}
}

fn use_next_shape(game: &mut Game) {
if let Some(ref mut piece) = game.piece {
piece.shape_kind = game.next_shape.clone();
if let Some(shape) = game.shapes.get(&piece.shape_kind) {
piece.pos.x = ((game.board.width as i32) - shape.aabb_width) / 2;
piece.pos.y = (game.board.height as i32) - shape.real_height;
piece.angle = PieceAngle::R0;
}
}
game.next_shape = get_random_shape(game);
}

fn get_random_shape(game: &mut Game) -> ShapeKind {
match game.terrible_rng % 7 {
0 => ShapeKind::I,
1 => ShapeKind::J,
2 => ShapeKind::L,
3 => ShapeKind::O,
4 => ShapeKind::S,
5 => ShapeKind::T,
6 => ShapeKind::Z,
_ => ShapeKind::Z, // never going to get here, but compiler insists
}
}

fn apply_lowering(game: &mut Game, delta: f32) -> bool {
game.time_to_next_lowering -= delta;
if game.time_to_next_lowering < 0.0 {
game.time_to_next_lowering = get_time_to_next_lowering(game.level);

if let Some(ref mut piece) = game.piece {
if let Some(shape) = game.shapes.get(&piece.shape_kind) {
if !lower_piece(&game.board, shape, piece) {
add_shape_to_board(&mut game.board, shape, &piece.pos, &piece.angle);
return true;
}
}
}
}
false
}

// if possible vertically drop the piece by one row, otherwise return false
//
fn lower_piece(board: &Board, shape: &Shape, piece: &mut Piece) -> bool {
let new_position = BoardPos { x: piece.pos.x, y: piece.pos.y - 1 };

if is_allowed(board, shape, &new_position, &piece.angle) {
piece.pos = new_position;
true
} else {
false
}
}

pub fn tick(&mut self, delta: f32, elapsed: f32) -> bool {
// always followed by a render
// returns true if tick should be called again
//
fn apply_user_input(game: &mut Game, controller: &Controller, delta: f32) {
if let Some(ref mut piece) = game.piece {
if let Some(shape) = game.shapes.get(&piece.shape_kind) {

if can_apply_input(&mut game.left_cooldown, controller.left, game.default_cooldown, delta) {
let new_position = BoardPos { x: piece.pos.x - 1, y: piece.pos.y };
if is_allowed(&game.board, shape, &new_position, &piece.angle) {
piece.pos = new_position;
}
}

if can_apply_input(&mut game.right_cooldown, controller.right, game.default_cooldown, delta) {
let new_position = BoardPos { x: piece.pos.x + 1, y: piece.pos.y };
if is_allowed(&game.board, shape, &new_position, &piece.angle) {
piece.pos = new_position;
}
}

if can_apply_input(&mut game.up_cooldown, controller.up, game.default_cooldown, delta) {
let new_angle;
if shape.fully_rotate {
new_angle = match piece.angle {
PieceAngle::R0 => PieceAngle::R270,
PieceAngle::R270 => PieceAngle::R180,
PieceAngle::R180 => PieceAngle::R90,
PieceAngle::R90 => PieceAngle::R0,
};
} else {
new_angle = match piece.angle {
PieceAngle::R0 => PieceAngle::R90,
_ => PieceAngle::R0,
};
}

if is_allowed(&game.board, shape, &piece.pos, &new_angle) {
piece.angle = new_angle;
}
}

if can_apply_input(&mut game.down_cooldown, controller.down, game.default_cooldown, delta) {
game.piece_is_dropping = true;
}

if can_apply_input(&mut game.a_cooldown, controller.a, game.default_cooldown, delta) {
let new_angle;
if shape.fully_rotate {
new_angle = match piece.angle {
PieceAngle::R0 => PieceAngle::R90,
PieceAngle::R90 => PieceAngle::R180,
PieceAngle::R180 => PieceAngle::R270,
PieceAngle::R270 => PieceAngle::R0,
};
} else {
new_angle = match piece.angle {
PieceAngle::R0 => PieceAngle::R90,
_ => PieceAngle::R0,
};
}

if is_allowed(&game.board, shape, &piece.pos, &new_angle) {
piece.angle = new_angle;
}
}

// prevent a crazy delta value if we're resuming from a paused state
if self.resume_from_paused {
self.resume_from_paused = false;
self.delta = 0.0;
if can_apply_input(&mut game.b_cooldown, controller.b, game.default_cooldown, delta) {
let new_angle;
if shape.fully_rotate {
new_angle = match piece.angle {
PieceAngle::R0 => PieceAngle::R270,
PieceAngle::R270 => PieceAngle::R180,
PieceAngle::R180 => PieceAngle::R90,
PieceAngle::R90 => PieceAngle::R0,
};
} else {
new_angle = match piece.angle {
PieceAngle::R0 => PieceAngle::R90,
_ => PieceAngle::R0,
};
}

if is_allowed(&game.board, shape, &piece.pos, &new_angle) {
piece.angle = new_angle;
}
}
}
}
}

// apply a cooldown effect on input that remains pressed, this way it is only
// acted upon every 'default_cooldown' ms
fn can_apply_input(cooldown: &mut f32, pressed: bool, default_cooldown: f32, delta: f32) -> bool {
if pressed {
if cooldown <= &mut 0.0 {
*cooldown = default_cooldown;
return true;
} else {
self.delta = delta;
*cooldown -= delta;
}
} else {
*cooldown = 0.0;
}
false
}

fn get_time_to_next_lowering(level: i32) -> f32 {
if level < 100 {
1000.0
} else {
500.0
}
}

fn is_allowed(board: &Board, shape: &Shape, pos: &BoardPos, angle: &PieceAngle) -> bool {
!is_outside(board, shape, pos, angle) && !is_colliding(board, shape, pos, angle)
}

fn is_outside(board: &Board, shape: &Shape, pos: &BoardPos, angle: &PieceAngle) -> bool {
let w = board.width as i32;

get_board_positions(shape, pos, angle)
.iter()
.any(|p| p.x < 0 || p.x >= w || p.y < 0)
}

fn is_colliding(board: &Board, shape: &Shape, pos: &BoardPos, angle: &PieceAngle) -> bool {
get_board_positions(shape, pos, angle)
.iter()
.filter(|p| is_valid_board_position(board, p))
.any(|p| board.board[p.y as usize][p.x as usize].active)
}

fn add_shape_to_board(board: &mut Board, shape: &Shape, pos: &BoardPos, angle: &PieceAngle) {
for pos in get_board_positions(shape, pos, angle) {
if is_valid_board_position(board, &pos) {
set_block(board, pos.x, pos.y, true);
}
}
}

match self.mode {
GameMode::Playing => self.state.tick_playing(&self.controller, delta, elapsed),
GameMode::Paused => false,
fn remove_shape_from_board(board: &mut Board, shape: &Shape, pos: &BoardPos, angle: &PieceAngle) {
for pos in get_board_positions(shape, &pos, &angle) {
if is_valid_board_position(board, &pos) {
set_block(board, pos.x, pos.y, false);
}
}
}

fn is_valid_board_position(board: &Board, pos: &BoardPos) -> bool {
let w = board.width as i32;
let h = board.height as i32;

pos.x >= 0 && pos.x < w && pos.y >= 0 && pos.y < h
}

fn get_board_positions(shape: &Shape, pos: &BoardPos, angle: &PieceAngle) -> Vec<BoardPos> {
let mut res: Vec<BoardPos> = Vec::with_capacity(4);
let mut x: i32;
let mut y: i32;

// use the current gamestate to update geometry
pub fn update_geometry(&mut self) {
self.state.update_geometry(&mut self.geometry);
self.state.update_geometry_debug(&mut self.geometry);
for y_offset in 0..shape.aabb_height {
for x_offset in 0..shape.aabb_width {
if shape.form[y_offset as usize][x_offset as usize] {

match angle {
PieceAngle::R0 => {
x = x_offset;
y = y_offset;
}
PieceAngle::R90 => {
x = y_offset;
y = shape.aabb_width - x_offset - 1;
},
PieceAngle::R180 => {
x = shape.aabb_width - x_offset - 1;
y = shape.aabb_height - y_offset - 1;
}
PieceAngle::R270 => {
x = shape.aabb_height - y_offset - 1;
y = x_offset;
}
}

res.push(BoardPos {x: pos.x + x, y: pos.y + y});
}
}
}

pub fn geo_len(&self) -> usize {
self.geometry.geo.len()
res
}

fn set_block(board: &mut Board, x: i32, y: i32, active: bool) {
board.board[y as usize][x as usize].active = active;
}

fn define_shapes() -> HashMap<ShapeKind, Shape> {
let mut res = HashMap::new();


// origin is in the bottom left
// indices are in [y][x] order
//
fn i_shape() -> Shape {
//
// -----
// __X__
// __X__
// __X__
// __X__
//
let mut v = vec![vec![false; 5]; 5];
v[0][2] = true;
v[1][2] = true;
v[2][2] = true;
v[3][2] = true;

Shape {
aabb_width: 5,
aabb_height: 5,
real_height: 4,
fully_rotate: false,
form: v,
}
}

fn o_shape() -> Shape {
//
// XX
// XX
//
let v = vec![vec![true; 2]; 2];

Shape {
aabb_width: 2,
aabb_height: 2,
real_height: 2,
fully_rotate: true,
form: v,
}
}

fn l_shape() -> Shape {
//
// _X_
// _X_
// _XX
//
let mut v = vec![vec![true; 3]; 3];
v[0][0] = false;
v[1][0] = false;
v[2][0] = false;
v[1][2] = false;
v[2][2] = false;

Shape {
aabb_width: 3,
aabb_height: 3,
real_height: 3,
fully_rotate: true,
form: v,
}
}

pub fn geo_ptr(&self) -> *const f32 {
self.geometry.geo.as_ptr() as *const f32
fn j_shape() -> Shape {
//
// _X_
// _X_
// XX_
//
let mut v = vec![vec![true; 3]; 3];
v[0][2] = false;
v[1][2] = false;
v[2][2] = false;
v[1][0] = false;
v[2][0] = false;

Shape {
aabb_width: 3,
aabb_height: 3,
real_height: 3,
fully_rotate: true,
form: v,
}
}

fn s_shape() -> Shape {
//
// ___
// _XX
// XX_
//
let mut v = vec![vec![true; 3]; 3];
v[0][2] = false;
v[1][0] = false;
v[2][0] = false;
v[2][1] = false;
v[2][2] = false;

Shape {
aabb_width: 3,
aabb_height: 3,
real_height: 2,
fully_rotate: false,
form: v,
}
}

fn t_shape() -> Shape {
//
// ___
// XXX
// _X_
//
let mut v = vec![vec![true; 3]; 3];
v[0][0] = false;
v[0][2] = false;
v[2][0] = false;
v[2][1] = false;
v[2][2] = false;

Shape {
aabb_width: 3,
aabb_height: 3,
real_height: 2,
fully_rotate: true,
form: v,
}
}

fn z_shape() -> Shape {
//
// ___
// XX_
// _XX
//
let mut v = vec![vec![true; 3]; 3];
v[0][0] = false;
v[1][2] = false;
v[2][0] = false;
v[2][1] = false;
v[2][2] = false;

Shape {
aabb_width: 3,
aabb_height: 3,
real_height: 2,
fully_rotate: false,
form: v,
}
}

res.insert(ShapeKind::I, i_shape());
res.insert(ShapeKind::J, j_shape());
res.insert(ShapeKind::L, l_shape());
res.insert(ShapeKind::O, o_shape());
res.insert(ShapeKind::S, s_shape());
res.insert(ShapeKind::T, t_shape());
res.insert(ShapeKind::Z, z_shape());

res
}

fn render_board(geometry: &mut Geometry, board: &Vec<Vec<Block>>, board_offset: &Block2D) {

let mut x = 0;
let mut y = 0;

for row in board {
for block in row {
let c = if block.active {
Col::new(1.0, 0.0, 0.0, 1.0)
} else {
Col::new(1.0, 1.0, 0.0, 0.1)
};

geometry.push_sprite(Sprite::Block,
c,
Block2D{ x: x + board_offset.x, y: y + board_offset.y },
block.offset);
x += 1;
}
x = 0;
y += 1;
}

}

fn render_score(geometry: &mut Geometry, score: i32, offset: Block2D, colour: Col) {
geometry.push_text("SCORE", offset, colour);
geometry.push_text(&score.to_string(), Block2D { x: offset.x, y: offset.y - 1 }, colour);
}

fn render_level(geometry: &mut Geometry, level: i32, offset: Block2D, colour: Col) {
geometry.push_text("LEVEL", offset, colour);
geometry.push_text(&level.to_string(), Block2D { x: offset.x, y: offset.y - 1 }, colour);
}

fn render_lines(geometry: &mut Geometry, lines: i32, offset: Block2D, colour: Col) {
geometry.push_text("LINES", offset, colour);
geometry.push_text(&lines.to_string(), Block2D { x: offset.x, y: offset.y - 1 }, colour);
}

fn render_next_shape(geometry: &mut Geometry, shape: &Shape, offset: Block2D, colour: Col) {
let board_positions = get_board_positions(shape, &BoardPos { x: 0, y: 0}, &PieceAngle::R0);
for pos in board_positions {
geometry.push_sprite(Sprite::Block,
colour,
Block2D {x: pos.x + offset.x, y: pos.y + offset.y},
Vec2D {x: 0.0, y: 0.0 });
}
}

+ 107
- 0
src/harbour.rs View File

@@ -0,0 +1,107 @@
use log;
use controller::{Controller, ControllerButton, ControllerAction};
use game::Game;
use geometry::Geometry;

#[derive(PartialEq)]
pub enum GameMode {
Playing,
Paused,
}

// harbour - a place of refuge.
//
pub struct Harbour {
delta: f32,

mode: GameMode,
resume_from_paused: bool,

controller: Controller,
game: Game,
geometry: Geometry,
}

impl Harbour {
pub fn new(
canvas_width: i32,
canvas_height: i32,
block_size: i32,
tileset_texture_width: i32, // tileset texture width
tileset_texture_height: i32, // tileset texture height
) -> Harbour {
Harbour {
delta: 0.0,

mode: GameMode::Playing,
resume_from_paused: false,

controller: Controller::new(),

game: Game::new(),

geometry: Geometry::new(canvas_width,
canvas_height,
block_size,
tileset_texture_width,
tileset_texture_height),
}
}

pub fn init(&mut self, random: f32) {
self.game.init(random);
}

// return true if we need to call tick after receiving an input event
// (only true when we resume playing after being paused)
pub fn input(&mut self, button: ControllerButton, action: ControllerAction) -> bool {
self.controller.input(button, action);

if button == ControllerButton::Start && action == ControllerAction::Up {
if self.mode == GameMode::Playing {
log("Paused");
self.mode = GameMode::Paused;
} else {
log("Playing");
self.mode = GameMode::Playing;
self.resume_from_paused = true;
return true;
}
}

false
}

pub fn tick(&mut self, delta: f32, elapsed: f32) -> bool {
// always followed by a render
// returns true if tick should be called again
//

// prevent a crazy delta value if we're resuming from a paused state
if self.resume_from_paused {
self.resume_from_paused = false;
self.delta = 0.0;
} else {
self.delta = delta;
}

match self.mode {
GameMode::Playing => self.game.tick_playing(&self.controller, delta, elapsed),
GameMode::Paused => false,
}
}

// use the current gamestate to update geometry
pub fn update_geometry(&mut self) {
self.game.update_geometry(&mut self.geometry);
self.game.update_geometry_debug(&mut self.geometry);
}

pub fn geo_len(&self) -> usize {
self.geometry.geo.len()
}

pub fn geo_ptr(&self) -> *const f32 {
self.geometry.geo.as_ptr() as *const f32
}
}

+ 28
- 26
src/lib.rs View File

@@ -8,13 +8,13 @@ use wasm_bindgen::prelude::*;
pub mod units;
pub mod sprite;
pub mod text;
pub mod harbour;
pub mod game;
pub mod state;
pub mod geometry;
pub mod controller;

use controller::{ControllerButton, ControllerAction};
use game::Game;
use harbour::Harbour;

#[wasm_bindgen(js_namespace = console)]
extern "C" {
@@ -39,12 +39,12 @@ impl KeyEventReturn {
}

#[wasm_bindgen]
pub struct GameBridge {
game: Game,
pub struct Bridge {
harbour: Harbour,
}

#[wasm_bindgen]
impl GameBridge {
impl Bridge {
#[wasm_bindgen(constructor)]
pub fn new(
canvas_width: i32,
@@ -52,57 +52,59 @@ impl GameBridge {
block_size: i32,
tileset_texture_width: i32, // tileset texture width
tileset_texture_height: i32, // tileset texture height
) -> GameBridge {
GameBridge {
game: Game::new(canvas_width, canvas_height,
block_size,
tileset_texture_width, tileset_texture_height)
) -> Bridge {
Bridge {
harbour: Harbour::new(canvas_width,
canvas_height,
block_size,
tileset_texture_width,
tileset_texture_height)
}
}

pub fn init(&mut self, random: f32) {
self.game.init(random);
self.harbour.init(random);
}

pub fn event_key_down(&mut self, key: String) -> KeyEventReturn {
input_event(&mut self.game, key, ControllerAction::Down)
input_event(&mut self.harbour, key, ControllerAction::Down)
}

pub fn event_key_up(&mut self, key: String) -> KeyEventReturn {
input_event(&mut self.game, key, ControllerAction::Up)
input_event(&mut self.harbour, key, ControllerAction::Up)
}

pub fn tick(&mut self, delta: f32, elapsed: f32) -> bool {
self.game.tick(delta, elapsed)
self.harbour.tick(delta, elapsed)
}

// use the current gamestate to update geometry
pub fn update_geometry(&mut self) {
self.game.update_geometry();
self.harbour.update_geometry();
}

pub fn geo_len(&self) -> usize {
self.game.geo_len()
self.harbour.geo_len()
}

pub fn geo_ptr(&self) -> *const f32 {
self.game.geo_ptr()
self.harbour.geo_ptr()
}
}

fn input_event(game: &mut Game, key: String, action: ControllerAction) -> KeyEventReturn {
fn input_event(harbour: &mut Harbour, key: String, action: ControllerAction) -> KeyEventReturn {
let mut prevent_default = true;
let mut call_tick = false;

match key.as_ref() {
"ArrowLeft" => call_tick = game.input(ControllerButton::Left, action),
"ArrowRight" => call_tick = game.input(ControllerButton::Right, action),
"ArrowUp" => call_tick = game.input(ControllerButton::Up, action),
"ArrowDown" => call_tick = game.input(ControllerButton::Down, action),
"a" => call_tick = game.input(ControllerButton::A, action),
"z" => call_tick = game.input(ControllerButton::B, action),
"Enter" => call_tick = game.input(ControllerButton::Start, action),
"Shift" => call_tick = game.input(ControllerButton::Select, action),
"ArrowLeft" => call_tick = harbour.input(ControllerButton::Left, action),
"ArrowRight" => call_tick = harbour.input(ControllerButton::Right, action),
"ArrowUp" => call_tick = harbour.input(ControllerButton::Up, action),
"ArrowDown" => call_tick = harbour.input(ControllerButton::Down, action),
"a" => call_tick = harbour.input(ControllerButton::A, action),
"z" => call_tick = harbour.input(ControllerButton::B, action),
"Enter" => call_tick = harbour.input(ControllerButton::Start, action),
"Shift" => call_tick = harbour.input(ControllerButton::Select, action),
_ => prevent_default = false,
}


+ 0
- 671
src/state.rs View File

@@ -1,671 +0,0 @@
use log;
use std::collections::HashMap;
use units::*;
use sprite::Sprite;
use controller::Controller;
use geometry::Geometry;

#[derive(Clone, Copy)]
pub struct Block {
active: bool,
offset: Vec2D
}

impl Block {
pub fn new() -> Block {
Block {
active: false,
offset: Vec2D {x: 0.0, y: 0.0 },
}
}
}

pub struct Board {
board: Vec<Vec<Block>>,
width: usize,
height: usize,
}

impl Board {
pub fn new(width: usize, height: usize) -> Board {
Board {
board: vec![vec![Block::new(); width]; height],
width,
height,
}
}
}

#[derive(Debug, Clone, Copy)]
pub struct BoardPos {
pub x: i32,
pub y: i32,
}

#[derive(Clone, Copy, Hash, Eq, PartialEq)]
enum ShapeKind {
I,
J,
L,
O,
S,
T,
Z,
}

pub struct Shape {
aabb_width: i32,
aabb_height: i32,
real_height: i32,
fully_rotate: bool,
form: Vec<Vec<bool>>,
}

enum PieceAngle {
R0,
R90,
R180,
R270,
}

pub struct Piece {
shape_kind: ShapeKind,
pos: BoardPos,
angle: PieceAngle,
}

pub struct State {
board: Board,
board_offset: Block2D,

shapes: HashMap<ShapeKind, Shape>,
piece: Option<Piece>, // the current piece under player control
next_shape: ShapeKind,

score: i32,
level: i32,
lines: i32,


time_to_next_lowering: f32, // ms
piece_is_dropping: bool,

default_cooldown: f32, // ms
left_cooldown: f32,
right_cooldown: f32,
up_cooldown: f32,
down_cooldown: f32,
a_cooldown: f32,
b_cooldown: f32,

terrible_rng: i32,
}

impl State {
pub fn new() -> State {
State {
board: Board::new(10, 20),
board_offset: Block2D { x: 5, y: 5 },

shapes: define_shapes(),
piece: Some(Piece {
shape_kind: ShapeKind::J,
pos: BoardPos {
x: 5,
y: 19,
},
angle: PieceAngle::R0,
}),
next_shape: ShapeKind::L,

score: 0,
level: 0,
lines: 0,

time_to_next_lowering: get_time_to_next_lowering(0),
piece_is_dropping: false,

default_cooldown: 100.0,
left_cooldown: 0.0,
right_cooldown: 0.0,
up_cooldown: 0.0,
down_cooldown: 0.0,
a_cooldown: 0.0,
b_cooldown: 0.0,

terrible_rng: 0
}
}

pub fn init(&mut self, random: f32) {
self.terrible_rng = (random * 100.0) as i32;
self.next_shape = get_random_shape(self);
self.terrible_rng = (random * 140.0) as i32;
use_next_shape(self);
}

pub fn update_geometry(&self, geometry: &mut Geometry) {
log("state: update_geometry");

geometry.clear();

// zero vert
geometry.push(0.0, 0.0, 1.0, 0.0, 1.0, 1.0, 0.0, 0.0);
render_board(geometry, &self.board.board, &self.board_offset);
render_score(geometry, self.score, Block2D { x: 19, y: 24 }, Col::new(1.0, 0.2, 0.3, 1.0));
render_level(geometry, self.level, Block2D { x: 19, y: 20 }, Col::new(1.0, 0.2, 0.3, 1.0));
render_lines(geometry, self.lines, Block2D { x: 19, y: 16 }, Col::new(1.0, 0.2, 0.3, 1.0));

if let Some(shape) = self.shapes.get(&self.next_shape) {
render_next_shape(geometry, &shape, Block2D { x: 19, y: 10 }, Col::new(1.0, 0.2, 0.3, 1.0));
}

}

pub fn update_geometry_debug(&self, _geometry: &mut Geometry) {

}

// counting on the fact that delta will always be a 'sensible' value.
// so if the player has paused the game for a while, on the first resumed call to tick_playing
// delta should be 0 rather than a large multi-second value
pub fn tick_playing(&mut self, controller: &Controller, delta: f32, elapsed: f32) -> bool {

self.terrible_rng = elapsed as i32;

if let Some(ref piece) = self.piece {
if let Some(shape) = self.shapes.get(&piece.shape_kind) {
remove_shape_from_board(&mut self.board, shape, &piece.pos, &piece.angle);
}
}

apply_user_input(self, controller, delta);

if self.piece_is_dropping {
if let Some(ref mut piece) = self.piece {
if let Some(shape) = self.shapes.get(&piece.shape_kind) {
let has_lowered = lower_piece(&self.board, shape, piece);
if !has_lowered {
self.piece_is_dropping = false;
}
}
}
} else if apply_lowering(self, delta) {
use_next_shape(self);
}

if let Some(ref piece) = self.piece {
if let Some(shape) = self.shapes.get(&piece.shape_kind) {
add_shape_to_board(&mut self.board, shape, &piece.pos, &piece.angle);
}
}

true
}
}

fn use_next_shape(state: &mut State) {
if let Some(ref mut piece) = state.piece {
piece.shape_kind = state.next_shape.clone();
if let Some(shape) = state.shapes.get(&piece.shape_kind) {
piece.pos.x = ((state.board.width as i32) - shape.aabb_width) / 2;
piece.pos.y = (state.board.height as i32) - shape.real_height;
piece.angle = PieceAngle::R0;
}
}
state.next_shape = get_random_shape(state);
}

fn get_random_shape(state: &mut State) -> ShapeKind {
match state.terrible_rng % 7 {
0 => ShapeKind::I,
1 => ShapeKind::J,
2 => ShapeKind::L,
3 => ShapeKind::O,
4 => ShapeKind::S,
5 => ShapeKind::T,
6 => ShapeKind::Z,
_ => ShapeKind::Z, // never going to get here, but compiler insists
}
}

fn apply_lowering(state: &mut State, delta: f32) -> bool {
state.time_to_next_lowering -= delta;
if state.time_to_next_lowering < 0.0 {
state.time_to_next_lowering = get_time_to_next_lowering(state.level);

if let Some(ref mut piece) = state.piece {
if let Some(shape) = state.shapes.get(&piece.shape_kind) {
if !lower_piece(&state.board, shape, piece) {
add_shape_to_board(&mut state.board, shape, &piece.pos, &piece.angle);
return true;
}
}
}
}
false
}

// if possible vertically drop the piece by one row, otherwise return false
//
fn lower_piece(board: &Board, shape: &Shape, piece: &mut Piece) -> bool {
let new_position = BoardPos { x: piece.pos.x, y: piece.pos.y - 1 };

if is_allowed(board, shape, &new_position, &piece.angle) {
piece.pos = new_position;
true
} else {
false
}
}

fn apply_user_input(state: &mut State, controller: &Controller, delta: f32) {
if let Some(ref mut piece) = state.piece {
if let Some(shape) = state.shapes.get(&piece.shape_kind) {

if can_apply_input(&mut state.left_cooldown, controller.left, state.default_cooldown, delta) {
let new_position = BoardPos { x: piece.pos.x - 1, y: piece.pos.y };
if is_allowed(&state.board, shape, &new_position, &piece.angle) {
piece.pos = new_position;
}
}

if can_apply_input(&mut state.right_cooldown, controller.right, state.default_cooldown, delta) {
let new_position = BoardPos { x: piece.pos.x + 1, y: piece.pos.y };
if is_allowed(&state.board, shape, &new_position, &piece.angle) {
piece.pos = new_position;
}
}

if can_apply_input(&mut state.up_cooldown, controller.up, state.default_cooldown, delta) {
let new_angle;
if shape.fully_rotate {
new_angle = match piece.angle {
PieceAngle::R0 => PieceAngle::R270,
PieceAngle::R270 => PieceAngle::R180,
PieceAngle::R180 => PieceAngle::R90,
PieceAngle::R90 => PieceAngle::R0,
};
} else {
new_angle = match piece.angle {
PieceAngle::R0 => PieceAngle::R90,
_ => PieceAngle::R0,
};
}

if is_allowed(&state.board, shape, &piece.pos, &new_angle) {
piece.angle = new_angle;
}
}

if can_apply_input(&mut state.down_cooldown, controller.down, state.default_cooldown, delta) {
state.piece_is_dropping = true;
}

if can_apply_input(&mut state.a_cooldown, controller.a, state.default_cooldown, delta) {
let new_angle;
if shape.fully_rotate {
new_angle = match piece.angle {
PieceAngle::R0 => PieceAngle::R90,
PieceAngle::R90 => PieceAngle::R180,
PieceAngle::R180 => PieceAngle::R270,
PieceAngle::R270 => PieceAngle::R0,
};
} else {
new_angle = match piece.angle {
PieceAngle::R0 => PieceAngle::R90,
_ => PieceAngle::R0,
};
}

if is_allowed(&state.board, shape, &piece.pos, &new_angle) {
piece.angle = new_angle;
}
}

if can_apply_input(&mut state.b_cooldown, controller.b, state.default_cooldown, delta) {
let new_angle;
if shape.fully_rotate {
new_angle = match piece.angle {
PieceAngle::R0 => PieceAngle::R270,
PieceAngle::R270 => PieceAngle::R180,
PieceAngle::R180 => PieceAngle::R90,
PieceAngle::R90 => PieceAngle::R0,
};
} else {
new_angle = match piece.angle {
PieceAngle::R0 => PieceAngle::R90,
_ => PieceAngle::R0,
};
}

if is_allowed(&state.board, shape, &piece.pos, &new_angle) {
piece.angle = new_angle;
}
}
}
}
}

// apply a cooldown effect on input that remains pressed, this way it is only
// acted upon every 'default_cooldown' ms
fn can_apply_input(cooldown: &mut f32, pressed: bool, default_cooldown: f32, delta: f32) -> bool {
if pressed {
if cooldown <= &mut 0.0 {
*cooldown = default_cooldown;
return true;
} else {
*cooldown -= delta;
}
} else {
*cooldown = 0.0;
}
false
}

fn get_time_to_next_lowering(level: i32) -> f32 {
if level < 100 {
1000.0
} else {
500.0
}
}

fn is_allowed(board: &Board, shape: &Shape, pos: &BoardPos, angle: &PieceAngle) -> bool {
!is_outside(board, shape, pos, angle) && !is_colliding(board, shape, pos, angle)
}

fn is_outside(board: &Board, shape: &Shape, pos: &BoardPos, angle: &PieceAngle) -> bool {
let w = board.width as i32;

get_board_positions(shape, pos, angle)
.iter()
.any(|p| p.x < 0 || p.x >= w || p.y < 0)
}

fn is_colliding(board: &Board, shape: &Shape, pos: &BoardPos, angle: &PieceAngle) -> bool {
get_board_positions(shape, pos, angle)
.iter()
.filter(|p| is_valid_board_position(board, p))
.any(|p| board.board[p.y as usize][p.x as usize].active)
}

fn add_shape_to_board(board: &mut Board, shape: &Shape, pos: &BoardPos, angle: &PieceAngle) {
for pos in get_board_positions(shape, pos, angle) {
if is_valid_board_position(board, &pos) {
set_block(board, pos.x, pos.y, true);
}
}
}

fn remove_shape_from_board(board: &mut Board, shape: &Shape, pos: &BoardPos, angle: &PieceAngle) {
for pos in get_board_positions(shape, &pos, &angle) {
if is_valid_board_position(board, &pos) {
set_block(board, pos.x, pos.y, false);
}
}
}

fn is_valid_board_position(board: &Board, pos: &BoardPos) -> bool {
let w = board.width as i32;
let h = board.height as i32;

pos.x >= 0 && pos.x < w && pos.y >= 0 && pos.y < h
}

fn get_board_positions(shape: &Shape, pos: &BoardPos, angle: &PieceAngle) -> Vec<BoardPos> {
let mut res: Vec<BoardPos> = Vec::with_capacity(4);
let mut x: i32;
let mut y: i32;

for y_offset in 0..shape.aabb_height {
for x_offset in 0..shape.aabb_width {
if shape.form[y_offset as usize][x_offset as usize] {

match angle {
PieceAngle::R0 => {
x = x_offset;
y = y_offset;
}
PieceAngle::R90 => {
x = y_offset;
y = shape.aabb_width - x_offset - 1;
},
PieceAngle::R180 => {
x = shape.aabb_width - x_offset - 1;
y = shape.aabb_height - y_offset - 1;
}
PieceAngle::R270 => {
x = shape.aabb_height - y_offset - 1;
y = x_offset;
}
}

res.push(BoardPos {x: pos.x + x, y: pos.y + y});
}
}
}

res
}

fn set_block(board: &mut Board, x: i32, y: i32, active: bool) {
board.board[y as usize][x as usize].active = active;
}

fn define_shapes() -> HashMap<ShapeKind, Shape> {
let mut res = HashMap::new();


// origin is in the bottom left
// indices are in [y][x] order
//
fn i_shape() -> Shape {
//
// -----
// __X__
// __X__
// __X__
// __X__
//
let mut v = vec![vec![false; 5]; 5];
v[0][2] = true;
v[1][2] = true;
v[2][2] = true;
v[3][2] = true;

Shape {
aabb_width: 5,
aabb_height: 5,
real_height: 4,
fully_rotate: false,
form: v,
}
}

fn o_shape() -> Shape {
//
// XX
// XX
//
let v = vec![vec![true; 2]; 2];

Shape {
aabb_width: 2,
aabb_height: 2,
real_height: 2,
fully_rotate: true,
form: v,
}
}

fn l_shape() -> Shape {
//
// _X_
// _X_
// _XX
//
let mut v = vec![vec![true; 3]; 3];
v[0][0] = false;
v[1][0] = false;
v[2][0] = false;
v[1][2] = false;
v[2][2] = false;

Shape {
aabb_width: 3,
aabb_height: 3,
real_height: 3,
fully_rotate: true,
form: v,
}
}

fn j_shape() -> Shape {
//
// _X_
// _X_
// XX_
//
let mut v = vec![vec![true; 3]; 3];
v[0][2] = false;
v[1][2] = false;
v[2][2] = false;
v[1][0] = false;
v[2][0] = false;

Shape {
aabb_width: 3,
aabb_height: 3,
real_height: 3,
fully_rotate: true,
form: v,
}
}

fn s_shape() -> Shape {
//
// ___
// _XX
// XX_
//
let mut v = vec![vec![true; 3]; 3];
v[0][2] = false;
v[1][0] = false;
v[2][0] = false;
v[2][1] = false;
v[2][2] = false;

Shape {
aabb_width: 3,
aabb_height: 3,
real_height: 2,
fully_rotate: false,
form: v,
}
}

fn t_shape() -> Shape {
//
// ___
// XXX
// _X_
//
let mut v = vec![vec![true; 3]; 3];
v[0][0] = false;
v[0][2] = false;
v[2][0] = false;
v[2][1] = false;
v[2][2] = false;

Shape {
aabb_width: 3,
aabb_height: 3,
real_height: 2,
fully_rotate: true,
form: v,
}
}

fn z_shape() -> Shape {
//
// ___
// XX_
// _XX
//
let mut v = vec![vec![true; 3]; 3];
v[0][0] = false;
v[1][2] = false;
v[2][0] = false;
v[2][1] = false;
v[2][2] = false;

Shape {
aabb_width: 3,
aabb_height: 3,
real_height: 2,
fully_rotate: false,
form: v,
}
}

res.insert(ShapeKind::I, i_shape());
res.insert(ShapeKind::J, j_shape());
res.insert(ShapeKind::L, l_shape());
res.insert(ShapeKind::O, o_shape());
res.insert(ShapeKind::S, s_shape());
res.insert(ShapeKind::T, t_shape());
res.insert(ShapeKind::Z, z_shape());

res
}

fn render_board(geometry: &mut Geometry, board: &Vec<Vec<Block>>, board_offset: &Block2D) {

let mut x = 0;
let mut y = 0;

for row in board {
for block in row {
let c = if block.active {
Col::new(1.0, 0.0, 0.0, 1.0)
} else {
Col::new(1.0, 1.0, 0.0, 0.1)
};

geometry.push_sprite(Sprite::Block,
c,
Block2D{ x: x + board_offset.x, y: y + board_offset.y },
block.offset);
x += 1;
}
x = 0;
y += 1;
}

}

fn render_score(geometry: &mut Geometry, score: i32, offset: Block2D, colour: Col) {
geometry.push_text("SCORE", offset, colour);
geometry.push_text(&score.to_string(), Block2D { x: offset.x, y: offset.y - 1 }, colour);
}

fn render_level(geometry: &mut Geometry, level: i32, offset: Block2D, colour: Col) {
geometry.push_text("LEVEL", offset, colour);
geometry.push_text(&level.to_string(), Block2D { x: offset.x, y: offset.y - 1 }, colour);
}

fn render_lines(geometry: &mut Geometry, lines: i32, offset: Block2D, colour: Col) {
geometry.push_text("LINES", offset, colour);
geometry.push_text(&lines.to_string(), Block2D { x: offset.x, y: offset.y - 1 }, colour);
}

fn render_next_shape(geometry: &mut Geometry, shape: &Shape, offset: Block2D, colour: Col) {
let board_positions = get_board_positions(shape, &BoardPos { x: 0, y: 0}, &PieceAngle::R0);
for pos in board_positions {
geometry.push_sprite(Sprite::Block,
colour,
Block2D {x: pos.x + offset.x, y: pos.y + offset.y},
Vec2D {x: 0.0, y: 0.0 });
}
}

+ 19
- 19
web/js/index.js View File

@@ -1,4 +1,4 @@
import { GameBridge } from "../wasm_tetris";
import { Bridge } from "../wasm_tetris";
import { memory } from "../wasm_tetris_bg";
import { GLRenderer } from "./GLRenderer";

@@ -14,7 +14,7 @@ let gState = {

tileset: '/web/img/tileset.png',

gameBridge: undefined,
bridge: undefined,
animationId: undefined,
renderer: undefined,

@@ -26,10 +26,10 @@ const tick = () => {
const delta = now - gState.performanceNow;
gState.performanceNow = now;

const gameBridge = gState.gameBridge;
const bridge = gState.bridge;

// game state + time + user input -> game state
const tickAgain = gameBridge.tick(delta, now);
const tickAgain = bridge.tick(delta, now);

render();

@@ -39,16 +39,16 @@ const tick = () => {
};

const render = () => {
const gameBridge = gState.gameBridge;
const bridge = gState.bridge;
const renderer = gState.renderer;

// gameBridge state -> geometry
gameBridge.update_geometry();
// bridge state -> geometry
bridge.update_geometry();

// render geometry
const memoryF32 = new Float32Array(memory.buffer);
const geo_len = gameBridge.geo_len();
const geo_ptr = gameBridge.geo_ptr();
const geo_len = bridge.geo_len();
const geo_ptr = bridge.geo_ptr();

renderer.preDrawScene(gState.canvasWidth, gState.canvasHeight);
renderer.drawBuffer(memoryF32, geo_len, geo_ptr);
@@ -61,16 +61,16 @@ function main() {

let renderer = new GLRenderer(gState, canvasElement);
renderer.loadTexture(gState.tileset).then(([tilesetWidth, tilesetHeight]) => {
gState.gameBridge = new GameBridge(gState.canvasWidth,
gState.canvasHeight,
gState.blockSize,
tilesetWidth,
tilesetHeight);
gState.bridge = new Bridge(gState.canvasWidth,
gState.canvasHeight,
gState.blockSize,
tilesetWidth,
tilesetHeight);

gState.renderer = renderer;

document.addEventListener('keydown', event => {
let key_event_return = gState.gameBridge.event_key_down(event.key);
let key_event_return = gState.bridge.event_key_down(event.key);

if (key_event_return.prevent_default()) {
event.preventDefault();
@@ -82,7 +82,7 @@ function main() {
key_event_return.free();
});
document.addEventListener('keyup', event => {
let key_event_return = gState.gameBridge.event_key_up(event.key);
let key_event_return = gState.bridge.event_key_up(event.key);

if (key_event_return.prevent_default()) {
event.preventDefault();
@@ -95,12 +95,12 @@ function main() {
});


gState.gameBridge.init(Math.random());
gState.bridge.init(Math.random());
tick();
}).catch(error => console.error(error));

// isg: NOTE: remember to free GameBridge
// gState.gameBridge.free();
// isg: NOTE: remember to free Bridge
// gState.bridge.free();
}

main();

Loading…
Cancel
Save