~sircmpwn/tetrominoes

cb6325f4a20fc17344c661daf03aaf79044dee8f — Drew DeVault 5 months ago e1bdb09
Rotate tetrominoes around their center
5 files changed, 150 insertions(+), 45 deletions(-)

M board.ha
M main.ha
M notes.txt
M tetrominoes.ha
M update.ha
M board.ha => board.ha +32 -5
@@ 1,5 1,6 @@
use math::random;
use sdl2;
use types;

def BOARD_WIDTH: int = 10;
def BOARD_HEIGHT: int = 20;


@@ 72,6 73,27 @@ fn draw_board(state: *state, board: *board) (void | sdl2::error) = {
	};

	draw_active(state, board.ghost_x, board.ghost_y, 128)?;

	const now = sdl2::get_ticks();
	switch (state.state) {
	case gamestate::INTRO =>
		// XXX: This is hacky as fuck.
		// https://todo.sr.ht/~sircmpwn/hare/54
		let alpha = 255u8;
		const duration = (state.fadein_finish - state.fadein_start): int;
		const diff = (now - state.fadein_start): int;
		let workaround = 1.0 - (diff: f64 / duration: f64);
		if (diff > duration) {
			workaround = 0.0;
		};
		if (now >= state.fadein_start) {
			alpha = (workaround * 255.0): int: u8;
		};
		sdl2::set_render_draw_color(state.render, 0, 0, 0, alpha)?;
		sdl2::render_fill_rect(state.render, null)?;
	case => void;
	};

	draw_active(state, board.active_x, board.active_y, 255)?;
};



@@ 90,7 112,7 @@ fn draw_active(
			continue;
		};
		const x = x + active.0, y = y + active.1;
		if (y < 0) {
		if (y < 0 && state.state != gamestate::INTRO) {
			continue;
		};
		let color = &color_map[cell];


@@ 151,8 173,6 @@ fn update_ghost(state: *state) void = {
	const board = &state.board;
	const cells = &board.cells;
	const active = &board.active[board.active_i];
	let width = 0, height = 0;
	activesize(state, &width, &height);

	let y = 1;
	for (canmove(state, active, 0, y); y += 1) void;


@@ 225,6 245,7 @@ fn activesize(
	width: nullable *int,
	height: nullable *int,
) void = {
	let min_x = types::INT_MAX, min_y = types::INT_MAX;
	let max_x = 0, max_y = 0;
	const active = &state.board.active[state.board.active_i];
	for (let y = 0; y < TETROMINO_HEIGHT; y += 1)


@@ 236,15 257,21 @@ fn activesize(
		if (active && y > max_y) {
			max_y = y;
		};
		if (active && x < min_x) {
			min_x = x;
		};
		if (active && y < min_y) {
			min_y = y;
		};
	};
	match (width) {
	case let ptr: *int =>
		*ptr = max_x + 1;
		*ptr = max_x - min_x + 1;
	case null => void;
	};
	match (height) {
	case let ptr: *int =>
		*ptr = max_y + 1;
		*ptr = max_y - min_y + 1;
	case null => void;
	};
};

M main.ha => main.ha +15 -2
@@ 15,6 15,7 @@ type texture = struct {
};

type gamestate = enum {
	INTRO,
	SPAWN,
	FALL,
	CLEAR,


@@ 43,6 44,10 @@ type state = struct {
	lastcolor: color,
	clearframe: int,
	clearlines: []int,

	// Intro
	fadein_start: u32,
	fadein_finish: u32,
};

export fn main() void = {


@@ 95,7 100,7 @@ fn run() (void | fs::error | sdl2::error) = {
			...
		},
		rand = random::init(seed),
		speed = 250,
		speed = 60 * 1000 / 84, // TODO: Set this to BPM properly
		...
	};
	defer sdl2::destroy_texture(state.piece.tex);


@@ 106,6 111,7 @@ fn run() (void | fs::error | sdl2::error) = {
	mixer::play_channel(0, state.music, 0)?;

	sdl2::set_texture_blend_mode(state.piece.tex, sdl2::blend_mode::BLEND)?;
	sdl2::set_render_draw_blend_mode(state.render, sdl2::blend_mode::BLEND)?;

	let controller: nullable *sdl2::gamecontroller = null;
	for (let i = 0; i < sdl2::numjoysticks()?; i += 1) {


@@ 125,8 131,15 @@ fn run() (void | fs::error | sdl2::error) = {
		sdl2::game_controller_close(c);
	};

	const now = sdl2::get_ticks();
	shuffle_tetrominoes(&state);
	do_spawn(&state, sdl2::get_ticks());
	do_spawn(&state, now);

	// Set up intro
	state.state = gamestate::INTRO;
	state.board.active_y -= 5;
	state.fadein_start = now + 4000;
	state.fadein_finish = now + 8000;

	for (state.run) {
		update(&state)?;

M notes.txt => notes.txt +21 -3
@@ 1,8 1,6 @@
- tetromino swapping
- rotate pieces around center origin
- music
- sound effects
- level files & timings
- level files & music timings
- fix thumbsticks
- scorekeeping/display
- keyboard input


@@ 11,6 9,7 @@

later:
- menu system
	- options (music/sfx volume)
- key/controller re-bindings
- custom piece designs
- local multiplayer


@@ 25,3 24,22 @@ later:

https://tetris.fandom.com/wiki/Tetris_Guideline
https://tetris.fandom.com/wiki/SRS
https://tetris.fandom.com/wiki/TGM_Rotation

music sources:
	loops:
	https://opengameart.org/content/a-journey-awaits
	https://opengameart.org/content/snowfall
	https://opengameart.org/content/funny-chase-8-bit-chiptune
	https://opengameart.org/content/raining-beat-loop-8-bit-cyber-style-0
	https://opengameart.org/content/menu-music

	themes:
	https://opengameart.org/content/rise-of-spirit
	https://opengameart.org/content/menu-music
	https://opengameart.org/content/erdreich-des-h%C3%BCnchenvolk
	https://opengameart.org/content/acoustic-jazz

animation for concert opening:
- dark screen, one falling piece as the clarinet solo comes in
- when the rest of the band comes in, the rest of the level fades in

M tetrominoes.ha => tetrominoes.ha +58 -22
@@ 9,22 9,22 @@ const tetrominoes: [_][][TETROMINO_LEN]color = [
			0, 0, 0, 0,
		],
		[
			1, 0, 0, 0,
			1, 0, 0, 0,
			1, 1, 0, 0,
			0, 1, 0, 0,
			0, 1, 0, 0,
			0, 0, 0, 0,
			0, 0, 0, 0
		],
		[
			0, 0, 0, 0,
			1, 1, 1, 0,
			1, 0, 0, 0,
			0, 0, 0, 0,
			0, 0, 0, 0,
		],
		[
			1, 0, 0, 0,
			1, 0, 0, 0,
			1, 1, 0, 0,
			0, 0, 0, 0
			0, 1, 0, 0,
			0, 1, 0, 0,
			0, 0, 0, 0,
		],
	],
	[


@@ 36,21 36,21 @@ const tetrominoes: [_][][TETROMINO_LEN]color = [
			0, 0, 0, 0,
		],
		[
			1, 1, 0, 0,
			1, 0, 0, 0,
			1, 0, 0, 0,
			0, 1, 1, 0,
			0, 1, 0, 0,
			0, 1, 0, 0,
			0, 0, 0, 0,
		],
		[
			0, 0, 0, 0,
			1, 1, 1, 0,
			0, 0, 1, 0,
			0, 0, 0, 0,
			0, 0, 0, 0,
		],
		[
			0, 1, 0, 0,
			0, 1, 0, 0,
			1, 1, 0, 0,
			0, 0, 1, 0,
			0, 0, 1, 0,
			0, 1, 1, 0,
			0, 0, 0, 0,
		],
	],


@@ 72,6 72,18 @@ const tetrominoes: [_][][TETROMINO_LEN]color = [
			0, 0, 0, 0,
		],
		[
			0, 1, 0, 0,
			0, 1, 1, 0,
			0, 0, 1, 0,
			0, 0, 0, 0,
		],
		[
			0, 0, 0, 0,
			0, 1, 1, 0,
			1, 1, 0, 0,
			0, 0, 0, 0,
		],
		[
			1, 0, 0, 0,
			1, 1, 0, 0,
			0, 1, 0, 0,


@@ 87,6 99,18 @@ const tetrominoes: [_][][TETROMINO_LEN]color = [
			0, 0, 0, 0,
		],
		[
			0, 0, 1, 0,
			0, 1, 1, 0,
			0, 1, 0, 0,
			0, 0, 0, 0,
		],
		[
			0, 0, 0, 0,
			1, 1, 0, 0,
			0, 1, 1, 0,
			0, 0, 0, 0,
		],
		[
			0, 1, 0, 0,
			1, 1, 0, 0,
			1, 0, 0, 0,


@@ 102,16 126,16 @@ const tetrominoes: [_][][TETROMINO_LEN]color = [
			0, 0, 0, 0,
		],
		[
			1, 0, 0, 0,
			1, 1, 0, 0,
			1, 0, 0, 0,
			0, 1, 0, 0,
			0, 1, 1, 0,
			0, 1, 0, 0,
			0, 0, 0, 0,
		],
		[
			0, 0, 0, 0,
			1, 1, 1, 0,
			0, 1, 0, 0,
			0, 0, 0, 0,
			0, 0, 0, 0,
		],
		[
			0, 1, 0, 0,


@@ 123,16 147,28 @@ const tetrominoes: [_][][TETROMINO_LEN]color = [
	[
		// |
		[
			0, 0, 0, 0,
			1, 1, 1, 1,
			0, 0, 0, 0,
			0, 0, 0, 0,
		],
		[
			0, 0, 1, 0,
			0, 0, 1, 0,
			0, 0, 1, 0,
			0, 0, 1, 0,
		],
		[
			0, 0, 0, 0,
			0, 0, 0, 0,
			1, 1, 1, 1,
			0, 0, 0, 0,
		],
		[
			1, 0, 0, 0,
			1, 0, 0, 0,
			1, 0, 0, 0,
			1, 0, 0, 0,
			0, 1, 0, 0,
			0, 1, 0, 0,
			0, 1, 0, 0,
			0, 1, 0, 0,
		],
	],
];

M update.ha => update.ha +24 -13
@@ 44,8 44,11 @@ fn update(state: *state) (void | sdl2::error) = {
				controller_button::X, controller_button::Y =>
			rotate(state);
		case controller_button::DPAD_UP =>
			state.board.active_y = state.board.ghost_y;
			commit(state);
			if (state.state != gamestate::INTRO) {
				state.board.active_y = state.board.ghost_y;
				commit(state);
				state.next = now;
			};
		case controller_button::DPAD_DOWN =>
			state.faster = true;
		case => void;


@@ 61,6 64,8 @@ fn update(state: *state) (void | sdl2::error) = {

	if (state.next <= now) {
		switch (state.state) {
		case gamestate::INTRO =>
			do_intro(state, now);
		case gamestate::SPAWN =>
			do_spawn(state, now);
		case gamestate::FALL =>


@@ 74,6 79,14 @@ fn update(state: *state) (void | sdl2::error) = {
	};
};

fn do_intro(state: *state, now: u32) void = {
	state.board.active_y += 1;
	if (state.fadein_finish <= now) {
		state.state = gamestate::FALL;
	};
	schedule_tick(state, now);
};

fn do_spawn(state: *state, now: u32) void = {
	let next = state.board.tetrominoes[state.board.next];
	state.board.next += 1;


@@ 107,7 120,7 @@ fn do_spawn(state: *state, now: u32) void = {

	let width = 0, height = 0;
	activesize(state, &width, &height);
	state.board.active_y = -height;
	state.board.active_y = -(height - 1);
	state.board.active_x = BOARD_WIDTH / 2 - width / 2;
	state.state = gamestate::FALL;
	update_ghost(state);


@@ 115,51 128,49 @@ fn do_spawn(state: *state, now: u32) void = {
};

fn do_fall(state: *state, now: u32) void = {
	let height = 0;
	activesize(state, null, &height);
	const active = &state.board.active[state.board.active_i];
	if (canmove(state, active, 0, 1)) {
		state.board.active_y += 1;
		schedule_tick(state, now);
	} else {
		commit(state);
	};
	schedule_tick(state, now);
};

fn do_clear(state: *state, now: u32) void = {
	state.clearframe += 1;
	state.clearframe %= 12;
	state.next = now + (state.speed / 6);
	state.next = now + 25; // XXX: Based on tempo?

	if (state.clearframe == 4) {
		clear_lines(state);
	};
	if (state.clearframe == 9) {
		shiftrows(state);
		state.state = gamestate::SPAWN;
		state.board.active_y = -4;
		shiftrows(state);
	};
};

fn schedule_tick(state: *state, now: u32) void = {
	if (state.faster) {
		state.next = now + state.speed / 4;
		state.next = now + 100; // TODO: Based on BPM multiplier
	} else {
		state.next = now + state.speed;
	};
};

fn move_left(state: *state) void = {
	if (state.board.active_x > 0) {
	const active = &state.board.active[state.board.active_i];
	if (canmove(state, active, -1, 0)) {
		state.board.active_x -= 1;
		update_ghost(state);
	};
};

fn move_right(state: *state) void = {
	let width = 0;
	activesize(state, &width, null);
	if (state.board.active_x < BOARD_WIDTH - width) {
	const active = &state.board.active[state.board.active_i];
	if (canmove(state, active, 1, 0)) {
		state.board.active_x += 1;
		update_ghost(state);
	};