Want to contribute? Fork me on Codeberg.org!
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
septadrop/septadrop/src/main.rs

598 lines
22 KiB

#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
2 years ago
use gcd::Gcd;
use rand::seq::SliceRandom;
use sfml::audio::*;
2 years ago
use sfml::graphics::*;
use sfml::system::*;
2 years ago
use sfml::window::*;
2 years ago
use std::io::Write;
mod structs;
use structs::*;
mod config;
use config::*;
pub fn audio(name: &str) -> String {
format!("{RES_AUDIO_PATH}/{name}.wav")
}
pub fn texture(name: &str) -> String {
format!("{RES_TEXTURES_PATH}/{name}.png")
}
fn get_level(lines: u32) -> u32 {
std::cmp::min(lines / LINES_PER_LEVEL, 15)
2 years ago
}
fn get_update_interval(level: u32) -> u32 {
// From Tetris Worlds, see https://harddrop.com/wiki/Tetris_Worlds#Gravity
((0.8 - (level as i32 - 1) as f32 * 0.007).powi(level as i32 - 1) * 1000.0) as u32
}
fn handle_resize_events(code: Key, current_scale: &mut u32, render_window: &mut RenderWindow, scale_file_path: &std::path::PathBuf) {
let new_current_scale = match code {
Key::NUM0 | Key::NUM1 => 1,
Key::NUM2 => 2,
Key::NUM4 => 4,
Key::HYPHEN => match current_scale {
2 => 1,
4 => 2,
_ => 1
},
Key::EQUAL => match current_scale {
1 => 2,
2 => 4,
4 => 4,
_ => 1
},
_ => *current_scale
};
if *current_scale != new_current_scale {
render_window.set_size(Vector2u::new(WINDOW_WIDTH, WINDOW_HEIGHT) * new_current_scale);
let mut file = std::fs::OpenOptions::new()
.write(true)
.open(scale_file_path)
.unwrap();
file.write_all(new_current_scale.to_string().as_bytes()).unwrap();
file.flush().unwrap();
*current_scale = new_current_scale;
}
}
fn main() {
2 years ago
let context_settings = ContextSettings::default();
let scale_file_path = home::home_dir().unwrap().join(".septadrop-scale");
let mut current_scale: u32;
if scale_file_path.exists() {
current_scale = std::fs::read_to_string(&scale_file_path)
.unwrap()
.trim()
.parse::<u32>()
.unwrap();
} else {
let mut scale_file = std::fs::File::create(&scale_file_path).unwrap();
write!(&mut scale_file, "1").unwrap();
current_scale = 1;
}
2 years ago
let mut window = RenderWindow::new(
VideoMode::new(WINDOW_WIDTH, WINDOW_HEIGHT, 16),
"septadrop",
Style::TITLEBAR | Style::CLOSE,
2 years ago
&context_settings,
2 years ago
);
if current_scale != 1 {
window.set_size(Vector2u::new(WINDOW_WIDTH, WINDOW_HEIGHT) * current_scale);
}
2 years ago
window.set_framerate_limit(FPS);
window.set_key_repeat_enabled(false);
let icon = Image::from_file(&texture("icon")).unwrap();
{
2 years ago
let Vector2u {
x: width,
y: height,
} = icon.size();
2 years ago
window.set_icon(width, height, icon.pixel_data());
}
let mut thread_rng = rand::thread_rng();
let block_types = BlockType::init_list();
2 years ago
2 years ago
let mut random_block = || Block::new(block_types.choose(&mut thread_rng).unwrap());
2 years ago
2 years ago
let mut block = random_block();
let mut next_block = random_block();
let mut grid = Grid::new();
2 years ago
let blocks_texture = Texture::from_file(&texture("blocks")).unwrap();
let mut sprite = Sprite::with_texture(&blocks_texture);
let background_texture = Texture::from_file(&texture("background")).unwrap();
let background = Sprite::with_texture(&background_texture);
let number_renderer = NumberRenderer::default();
let mut paused_clear = RectangleShape::new();
paused_clear.set_fill_color(Color::rgb(81, 62, 69));
let paused_texture = Texture::from_file(&texture("paused")).unwrap();
let paused_text = {
let mut paused_text = Sprite::with_texture(&paused_texture);
let paused_texture_size = paused_texture.size();
paused_text.set_position(Vector2f::new(
2 years ago
PLAYFIELD_X as f32 + (GRID_WIDTH as f32 * TILE_SIZE as f32 / 2.0)
- paused_texture_size.x as f32 / 2.0,
PLAYFIELD_Y as f32 + (GRID_HEIGHT as f32 * TILE_SIZE as f32 / 2.0)
- paused_texture_size.y as f32 / 2.0,
2 years ago
));
paused_text
};
let highscore_file_path = home::home_dir().unwrap().join(".septadrop");
let mut highscore: u32;
if highscore_file_path.exists() {
2 years ago
highscore = std::fs::read_to_string(&highscore_file_path)
.unwrap()
.trim()
.parse::<u32>()
.unwrap();
2 years ago
let point_gcd = POINTS_1_LINE
.gcd(POINTS_2_LINES)
.gcd(POINTS_3_LINES)
.gcd(POINTS_4_LINES);
if highscore % point_gcd != 0 {
println!("It seems your system is misconfigured. Please see this guide for fixing the issue: https://www.youtube.com/watch?v=dQw4w9WgXcQ");
return;
}
} else {
let mut highscore_file = std::fs::File::create(&highscore_file_path).unwrap();
write!(&mut highscore_file, "0").unwrap();
highscore = 0;
}
let mut score: u32 = 0;
let mut lines: u32 = 0;
let mut blocks: u32 = 0;
let mut tiles: u32 = 0;
let mut update_interval = get_update_interval(0);
let mut rotate = false;
let mut move_left = false;
let mut move_right = false;
let mut move_left_immediate = false;
let mut move_right_immediate = false;
let mut fast_forward = false;
let mut snap = false;
let mut paused = false;
let mut paused_from_lost_focus = false;
let mut update_clock = Clock::default();
let mut move_clock = Clock::default();
let mut pause_clock = Clock::default();
let mut pause_offset: u32 = 0;
let mut toggle_pause = false;
// https://sfxr.me/#57uBnWWZeyDTsBRrJsAp2Vwd76cMVrdeRQ7DirNQW5XekKxcrCUNx47Zggh7Uqw4R5FdeUpyk362uhjWmpNHmqxE7JBp3EkxDxfJ1VjzMRpuSHieW6B5iyVFM
let rotate_buffer = SoundBuffer::from_file(&audio("rotate")).unwrap();
let mut rotate_sound = Sound::with_buffer(&rotate_buffer);
// https://sfxr.me/#57uBnWTMa2LUtaPa3P8xWZekiRxNwCPFWpRoPDVXDJM9KHkiGJcs6J62FRcjMY5oVNdT73MtmUf5rXCPvSZWL7AZuTRWWjKbPKTpZjT85AcZ6htUqTswkjksZ
let snap_buffer = SoundBuffer::from_file(&audio("snap")).unwrap();
let mut snap_sound = Sound::with_buffer(&snap_buffer);
// https://sfxr.me/#57uBnWbareN7MJJsWGD8eFCrqjikS9f8JXg8jvmKzMdVtqmRsb81eToSUpnkqgFhvxD2QoAjpw4SmGZHZjbhEiPQKetRSHCHXYFZzD7Q6RVVS9CRSeRAb6bZp
let game_over_buffer = SoundBuffer::from_file(&audio("game_over")).unwrap();
let mut game_over_sound = Sound::with_buffer(&game_over_buffer);
// https://sfxr.me/#7BMHBGMfGk8EHV8czJkUucUm8EMAnMNxiqYyTfKkMpHFJu44GEdD7xP6E8NM3K7RKRExTpagPBAiWf7BLtC52CEWJVGHh8hwDLygoEG86tcPth2UtmfdrXLoh
let row_clear_buffer = SoundBuffer::from_file(&audio("row_clear")).unwrap();
let mut row_clear_sound = Sound::with_buffer(&row_clear_buffer);
// https://sfxr.me/#57uBnWg8448kTPqWAxeDvZ5CP5JWbrfJGWuRcTjva5uX3vvBnEAZ6SfiH9oLKMXgsusuJwGWx6KPfvLfHtqnhLxr476ptGv4jPbfNhQaFMYeMHFdHk9SotQ4X
let level_up_buffer = SoundBuffer::from_file(&audio("level_up")).unwrap();
let mut level_up_sound = Sound::with_buffer(&level_up_buffer);
// https://sfxr.me/#34T6PkzvrkfdahGDBAh1uYGXTwZ8rG54kxfHpgdVCPxqG7yyK5UuqgiK9Z8Q5177itxbkSNfLSHm4zTkemT4iyxJpW89VJx82feaq8qxZeA5AJR2nWZZR59hq
let new_highscore_buffer = SoundBuffer::from_file(&audio("new_highscore")).unwrap();
let mut new_highscore_sound = Sound::with_buffer(&new_highscore_buffer);
while window.is_open() {
while let Some(event) = window.poll_event() {
match event {
Event::Closed | Event::KeyPressed {
code: Key::Q,
alt: _,
ctrl: true,
shift: _,
system: _,
} => window.close(),
Event::LostFocus => {
toggle_pause = true;
paused_from_lost_focus = true;
}
Event::KeyPressed {
code,
alt: _,
ctrl: true,
shift: _,
system: _,
} => {
handle_resize_events(code, &mut current_scale, &mut window, &scale_file_path);
},
Event::KeyPressed {
code,
alt: _,
ctrl: _,
shift: _,
system: _,
} => match code {
Key::ESCAPE => toggle_pause = true,
Key::SPACE => snap = true,
Key::UP => rotate = true,
Key::DOWN => fast_forward = true,
Key::LEFT => {
move_left = true;
move_left_immediate = true;
move_clock.restart();
}
Key::RIGHT => {
move_right = true;
move_right_immediate = true;
move_clock.restart();
2 years ago
}
_ => {}
2 years ago
},
Event::KeyReleased {
code,
alt: _,
ctrl: _,
shift: _,
system: _,
} => match code {
Key::DOWN => fast_forward = false,
Key::LEFT => move_left = false,
Key::RIGHT => move_right = false,
_ => {}
},
_ => {}
2 years ago
}
}
if toggle_pause {
paused = !paused;
if paused {
pause_clock.restart();
2 years ago
paused_clear.set_position(Vector2f::new(PLAYFIELD_X as f32, PLAYFIELD_Y as f32));
2 years ago
paused_clear.set_size(Vector2f::new(
(GRID_WIDTH * TILE_SIZE) as f32,
2 years ago
(GRID_HEIGHT * TILE_SIZE) as f32,
2 years ago
));
window.draw(&paused_clear);
let size = Vector2f::new(
(NEXT_WIDTH * TILE_SIZE) as f32,
2 years ago
(NEXT_HEIGHT * TILE_SIZE) as f32,
2 years ago
);
2 years ago
paused_clear.set_position(Vector2f::new(NEXT_X as f32, NEXT_Y as f32) - size / 2.0);
2 years ago
paused_clear.set_size(size);
window.draw(&paused_clear);
window.draw(&paused_text);
window.display();
} else {
pause_offset = pause_clock.elapsed_time().as_milliseconds() as u32;
}
toggle_pause = false;
}
if paused {
2 years ago
loop {
if let Some(event) = window.wait_event() {
match event {
Event::Closed | Event::KeyPressed {
code: Key::Q,
alt: _,
ctrl: true,
shift: _,
system: _,
} => window.close(),
Event::KeyPressed {
code,
alt: _,
ctrl: true,
shift: _,
system: _,
} => {
handle_resize_events(code, &mut current_scale, &mut window, &scale_file_path);
}
2 years ago
Event::KeyPressed {
code: Key::ESCAPE,
alt: _,
ctrl: _,
shift: _,
system: _,
} => {
paused = false;
break;
},
Event::GainedFocus => if paused_from_lost_focus {
paused = false;
paused_from_lost_focus = false;
break;
},
_ => {}
}
}
}
2 years ago
}
2 years ago
let is_update_frame = update_clock.elapsed_time().as_milliseconds() - pause_offset as i32
> if fast_forward {
2 years ago
std::cmp::min(update_interval, MAX_FAST_FORWARD_INTERVAL)
} else {
update_interval
} as i32;
if is_update_frame {
update_clock.restart();
}
2 years ago
let is_move_frame = move_clock.elapsed_time().as_milliseconds() - pause_offset as i32
> MOVE_FRAME_INTERVAL as i32;
2 years ago
if is_move_frame {
move_clock.restart();
}
pause_offset = 0;
// Rotation
if rotate {
block.rotation_state += 1;
// Check to see if new rotation state is overlapping any tiles
let mut offset_required: i32 = 0;
for tile in block.get_tiles().iter() {
if grid.get(tile.x, tile.y).is_some() {
2 years ago
// Can't wall kick off of blocks
block.rotation_state -= 1;
break;
}
if tile.x <= 0 {
let potential_offset = -tile.x;
if potential_offset > offset_required.abs() {
offset_required = potential_offset;
}
} else if tile.x >= GRID_WIDTH as i32 {
let potential_offset = GRID_WIDTH as i32 - tile.x - 1;
if -potential_offset > offset_required.abs() {
offset_required = potential_offset;
}
}
}
block.position.x += offset_required;
rotate = false;
rotate_sound.play();
}
// Horizontal movement
{
let mut movement = 0;
if move_left_immediate || (is_move_frame && move_left) {
movement -= 1;
move_left_immediate = false;
}
if move_right_immediate || (is_move_frame && move_right) {
movement += 1;
move_right_immediate = false;
}
if movement != 0 {
for (i, tile) in block.get_tiles().iter().enumerate().rev() {
if grid.filled(tile.x + movement, tile.y)
2 years ago
{
2 years ago
break;
}
if i == 0 {
/*
We're going through all the blocks backwards,
so the first element is last.
(Only for .enumerate().rev(), not .rev().enumerate(),
since .enumerate() is what adds indexes.)
If we managed to get through all of the tiles without breaking,
(haven't found anything that obstructs any tile),
we can finally add the movement.
*/
block.position.x += movement;
}
}
}
}
// Snapping
let snap_offset = {
let mut snap_offset = 0;
'outer: loop {
for tile in block.get_tiles().iter() {
let y = tile.y + snap_offset;
if grid.filled(tile.x, y) {
2 years ago
snap_offset -= 1;
break 'outer;
}
}
snap_offset += 1;
}
snap_offset
};
let mut landed = snap;
if snap {
block.position.y += snap_offset;
snap = false;
snap_sound.play();
2 years ago
} else if is_update_frame {
// Land checking
2 years ago
for tile in block.get_tiles().iter() {
2 years ago
if tile.y == GRID_HEIGHT as i32 - 1
|| grid.filled(tile.x, tile.y + 1)
2 years ago
{
2 years ago
landed = true;
break;
}
}
}
let landed = landed; // remove mutability
2 years ago
// Clear window
// Normally, one would run window.clear(),
// but the background image covers the entire window.
window.draw(&background);
2 years ago
// Draw block
if !landed {
for tile in block.get_tiles().iter() {
let snap_y = tile.y + snap_offset;
sprite.set_texture_rect(&block.block_type.tile_type.texture_rect);
sprite.set_position(Vector2f::new(
(PLAYFIELD_X as i32 + tile.x * TILE_SIZE as i32) as f32,
2 years ago
(PLAYFIELD_Y as i32 + tile.y * TILE_SIZE as i32) as f32,
2 years ago
));
window.draw(&sprite);
sprite.set_texture_rect(&block.block_type.tile_type.ghost_texture_rect);
sprite.set_position(Vector2f::new(
(PLAYFIELD_X as i32 + tile.x * TILE_SIZE as i32) as f32,
2 years ago
(PLAYFIELD_Y as i32 + snap_y * TILE_SIZE as i32) as f32,
2 years ago
));
window.draw(&sprite);
}
}
// Draw next block
{
let next_block_tiles = next_block.get_tiles();
// This is assuming the next block spawns unrotated.
// Refactoring is needed if random rotations are added
let x_offset = next_block.block_type.width * TILE_SIZE / 2;
2 years ago
let y_offset = (next_block.block_type.height + next_block.block_type.starting_line * 2)
* TILE_SIZE
/ 2;
2 years ago
for tile in next_block_tiles.iter() {
sprite.set_texture_rect(&next_block.block_type.tile_type.texture_rect);
sprite.set_position(Vector2f::new(
2 years ago
(NEXT_X + (tile.x - next_block.position.x) as u32 * TILE_SIZE - x_offset)
as f32,
(NEXT_Y + (tile.y - next_block.position.y) as u32 * TILE_SIZE - y_offset)
as f32,
2 years ago
));
window.draw(&sprite);
}
}
// Landing (transferring block to grid and reinitializing)
if landed {
tiles += block.get_tiles().len() as u32;
blocks += 1;
for tile in block.get_tiles().iter() {
grid.set(tile.x, tile.y, Some(&block.block_type.tile_type));
2 years ago
}
let mut cleared_lines = 0;
for y in 0..GRID_HEIGHT {
let mut completed = true;
for x in 0..GRID_WIDTH {
if !grid.filled(x as i32, y as i32) {
2 years ago
completed = false;
break;
}
}
if !completed {
continue;
}
for z in (0..y).rev() {
let z = z as i32;
2 years ago
for x in 0..GRID_WIDTH {
let x = x as i32;
grid.set(x, z + 1, grid.get(x, z));
2 years ago
}
}
cleared_lines += 1;
}
let mut scored = match cleared_lines {
0 => 0,
1 => POINTS_1_LINE,
2 => POINTS_2_LINES,
3 => POINTS_3_LINES,
2 years ago
_ => POINTS_4_LINES,
2 years ago
};
if scored > 0 {
let level = get_level(lines);
scored *= level + 1;
if score + scored > highscore && score < highscore {
new_highscore_sound.play();
}
score += scored;
lines += cleared_lines;
let new_level = get_level(lines);
if level != new_level {
level_up_sound.play();
}
if score > highscore {
highscore = score;
let mut file = std::fs::OpenOptions::new()
.write(true)
.open(&highscore_file_path)
.unwrap();
file.write_all(highscore.to_string().as_bytes()).unwrap();
file.flush().unwrap();
}
update_interval = get_update_interval(new_level);
row_clear_sound.play();
}
for tile in next_block.get_tiles().iter() {
if grid.filled(tile.x, tile.y) {
2 years ago
score = 0;
lines = 0;
blocks = 0;
tiles = 0;
grid.clear();
2 years ago
update_interval = get_update_interval(0);
game_over_sound.play();
next_block = random_block();
break;
}
}
block = next_block;
next_block = random_block();
} else if is_update_frame {
block.position.y += 1;
}
// Drawing grid
for y in 0..GRID_HEIGHT {
for x in 0..GRID_WIDTH {
let tile_type = grid.get(x as i32, y as i32);
2 years ago
if tile_type.is_none() {
continue;
}
let tile_type = tile_type.unwrap();
sprite.set_texture_rect(&tile_type.texture_rect);
sprite.set_position(Vector2f::new(
(PLAYFIELD_X + x * TILE_SIZE) as f32,
2 years ago
(PLAYFIELD_Y + y * TILE_SIZE) as f32,
2 years ago
));
window.draw(&sprite);
}
}
number_renderer.render(&mut window, score, 477, 162);
number_renderer.render(&mut window, highscore, 477, 202);
number_renderer.render(&mut window, lines, 477, 242);
number_renderer.render(&mut window, get_level(lines), 477, 282);
number_renderer.render(&mut window, blocks, 477, 322);
number_renderer.render(&mut window, tiles, 477, 362);
window.display();
}
}