Skip to content

Commit

Permalink
Allow game pausing while clearing lines
Browse files Browse the repository at this point in the history
So far we have simply (and silently) ignored "requests" to pause the
game while lines were being cleared. Conceptually that is a very small
time frame and it should not matter all that much. In reality, it was
mildly irritating to see a key press just being ignored in a fast paced
game.
With this change we make game pausing an unconditional operation: a
"request" will always be satisfied. The main obstacle to doing that in
the past was that if a clearing "animation" was in progress, it wasn't
quite clear how we would convey the end of the animation once the game
is resumed. With this change we solve this issue by just eagerly
transitioning from "clearing" state to "moving", arguing that it is
actually awkward to have a clearing animation continue after a resume
(if one was in progress while pausing).
  • Loading branch information
d-e-s-o committed Aug 10, 2024
1 parent 6008cb4 commit 6b16b42
Show file tree
Hide file tree
Showing 7 changed files with 49 additions and 20 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/publish-mode.yml
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ jobs:
--request POST \
--url https://api.github.com/repos/${{ github.repository }}/releases \
--header "Accept: application/vnd.github+json" \
--header "Authorization: Bearer ${{ secrets.GITHUB_TOKEN }}"\
--header "Authorization: Bearer ${{ secrets.GITHUB_TOKEN }}" \
--header "X-GitHub-Api-Version: 2022-11-28" \
--data "{
\"tag_name\":\"mode-v${version}\",
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/publish-xlock.yml
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ jobs:
--request POST \
--url https://api.github.com/repos/${{ github.repository }}/releases \
--header "Accept: application/vnd.github+json" \
--header "Authorization: Bearer ${{ secrets.GITHUB_TOKEN }}"\
--header "Authorization: Bearer ${{ secrets.GITHUB_TOKEN }}" \
--header "X-GitHub-Api-Version: 2022-11-28" \
--data "{
\"tag_name\":\"xlock-v${version}\",
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ jobs:
--request POST \
--url https://api.github.com/repos/${{ github.repository }}/releases \
--header "Accept: application/vnd.github+json" \
--header "Authorization: Bearer ${{ secrets.GITHUB_TOKEN }}"\
--header "Authorization: Bearer ${{ secrets.GITHUB_TOKEN }}" \
--header "X-GitHub-Api-Version: 2022-11-28" \
--data "{
\"tag_name\":\"v${version}\",
Expand Down
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
Unreleased
----------
- Allow for game pausing even while clearing lines


0.2.0
-----
- Updated `winit` dependency to `0.30`
Expand Down
21 changes: 20 additions & 1 deletion src/game/field.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright (C) 2023 Daniel Mueller <deso@posteo.net>
// Copyright (C) 2023-2024 Daniel Mueller <deso@posteo.net>
// SPDX-License-Identifier: GPL-3.0-or-later

use std::mem::replace;
Expand Down Expand Up @@ -259,6 +259,25 @@ impl Field {
self.rotate_stone(false)
}

/// "Event handler" for informing the field that the overall game has
/// been paused.
pub(super) fn on_pause(&mut self) {
match &mut self.state {
State::Clearing {
next_stone,
y_range,
..
} => {
let _removed = self.pieces.remove_complete_lines(y_range.clone());
self.state = State::Moving {
stone: next_stone.take(),
};
},
State::Colliding { .. } => panic!("attempted to pause from collision state"),
State::Moving { .. } => (),
}
}

/// Render the walls of the field.
fn render_walls(&self, renderer: &Renderer) {
let _guard = renderer.set_texture(&self.wall);
Expand Down
27 changes: 17 additions & 10 deletions src/game/game.rs
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,10 @@ impl Game {
match self.field.state() {
State::Moving { .. } => (),
State::Clearing { until, .. } => {
// The game must not be paused while we are clearing. Pausing
// should always transition the field to "moving" state.
debug_assert!(self.next_tick.is_some());

if now > *until {
self.next_tick = Some(Self::next_tick(*until, self.score.level()));
let () = self.field.clear_complete_lines();
Expand Down Expand Up @@ -289,10 +293,17 @@ impl Game {
/// Pause or unpause the game.
#[inline]
pub(crate) fn pause(&mut self, pause: bool) {
if pause {
let _next_tick = self.next_tick.take();
} else {
if !matches!(self.field.state(), State::Colliding { .. }) {
if !matches!(self.field.state(), State::Colliding { .. }) {
if pause {
// Note that strictly speaking the field could change state here
// (if it was "clearing") and, conceptually, we should cause a
// redraw (i.e., by returning `Change::Changed`. Practically,
// though, we do *not* want to do that, because doing so could
// eagerly remove cleared lines and it just makes more sense to
// leave them there for the duration of the pause.
let () = self.field.on_pause();
let _next_tick = self.next_tick.take();
} else {
let _next_tick = self
.next_tick
.replace(Self::next_tick(Instant::now(), self.score.level()));
Expand All @@ -302,12 +313,8 @@ impl Game {

/// Inquire whether the game is currently paused.
#[inline]
pub(crate) fn is_paused(&self) -> Option<bool> {
if matches!(self.field.state(), State::Colliding { .. }) {
None
} else {
Some(self.next_tick.is_none())
}
pub(crate) fn is_paused(&self) -> bool {
self.next_tick.is_none()
}

/// Enable or disable auto-playing of the game.
Expand Down
10 changes: 4 additions & 6 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -166,7 +166,7 @@ struct State {
game: Game,
renderer: Renderer,
keys: Keys,
was_paused: Option<bool>,
was_paused: bool,
}


Expand Down Expand Up @@ -236,14 +236,14 @@ impl ApplicationHandler for App {
let change = match event {
WindowEvent::Focused(focused) => {
if focused {
if let Some(false) = was_paused {
if !*was_paused {
// The game was not paused when we lost focus. That means
// we ended up pausing it. Unpause it again.
let () = game.pause(false);
}
} else {
*was_paused = game.is_paused();
if let Some(false) = was_paused {
if !*was_paused {
// The game is currently running but we are about to loose
// focus. Pause it, as the user will no longer have a
// chance to control it and it's not great to have it
Expand Down Expand Up @@ -347,9 +347,7 @@ impl ApplicationHandler for App {
Change::Unchanged
},
Key::F3 => {
if let Some(paused) = game.is_paused() {
let () = game.pause(!paused);
}
let () = game.pause(!game.is_paused());
*repeat = KeyRepeat::Disabled;
Change::Unchanged
},
Expand Down

0 comments on commit 6b16b42

Please sign in to comment.