1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
use std::{num::NonZeroU8, time::Duration};
use rand::Rng;
use falling_tetromino_engine::{
Button, DelayParameters, ExtDuration, Game, GameAccess, GameBuilder, GameLimits, GameModifier,
GameRng, InGameTime, Input, Line, NotificationFeed, Phase, Piece, Stat, Tetromino,
};
#[derive(
PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Debug, serde::Serialize, serde::Deserialize,
)]
pub struct Ascent {
height_loaded: usize,
}
impl Ascent {
pub const MOD_ID: &str = stringify!(Ascent);
pub fn build(builder: &GameBuilder) -> Game {
let modifier = Box::new(Self { height_loaded: 0 });
builder
.clone()
.lock_delay_params(DelayParameters::constant(ExtDuration::Infinite))
.game_limits(GameLimits::single(
Stat::TimeElapsed(Duration::from_secs(2 * 60)),
true,
))
.build_modded(vec![modifier])
}
}
impl GameModifier for Ascent {
fn id(&self) -> String {
Self::MOD_ID.to_owned()
}
fn args(&self) -> String {
"".to_owned()
}
fn try_clone(&self) -> Result<Box<dyn GameModifier>, String> {
Ok(Box::new(self.clone()))
}
fn on_game_built(&mut self, game: GameAccess) {
// Load in board.
let ascent_lines = Self::prng_ascent_lines(&mut self.height_loaded, &mut game.state.rng);
for (line, ascent_line) in game
.state
.board
.iter_mut()
.take(Game::HEIGHT)
.zip(ascent_lines)
{
*line = ascent_line;
}
// Manually place active piece.
let asc_tet_01 = Tetromino::L;
let asc_tet_02 = Tetromino::J;
*game.phase = Phase::PieceInPlay {
piece: Piece {
tetromino: asc_tet_01,
orientation: falling_tetromino_engine::Orientation::N,
position: (0, 0),
},
auto_move_scheduled: None,
fall_or_lock_time: Duration::MAX,
lowest_y: 0,
lock_time_cap: Duration::MAX,
};
// Provide hold piece.
game.state.piece_held = Some((asc_tet_02, true));
// No other pieces required.
game.config.piece_preview_count = 0;
}
// The Ascent mod must keep scoring after each piece change.
// It must also adjust the 'camera' - visible board and piece state to simulate 'ascending'.
fn on_player_action_post(
&mut self,
game: GameAccess,
_feed: &mut NotificationFeed,
input: Input,
) {
// In this mode, only rotating the pieces can change it.
// FIXME: We're forgetting 'Hold' could as well (e.g. when swap touches new gem). Experimental gamemode though.
if !matches!(
input,
Input::Activate(Button::RotateLeft | Button::Rotate180 | Button::RotateRight)
) {
return;
}
// Guaranteed to be in `Phase::PieceInPlay`.
let piece = game.phase.piece_mut().unwrap();
let piece_tiles_coords = piece.tiles().map(|(coord, _)| coord);
// Update entire board by cycling colors.
for (y, line) in game.state.board.iter_mut().enumerate() {
for (x, tile) in line.iter_mut().take(Self::PLAYABLE_WIDTH).enumerate() {
let Some(tiletypeid) = tile else {
continue;
};
let i = tiletypeid.get();
// Modify only certain tiles.
if i <= 7 {
// Piece is touching the tile.
let tilenum = if piece_tiles_coords.iter().any(|&(x_p, y_p)| {
(x_p as usize).abs_diff(x) + (y_p as usize).abs_diff(y) <= 1
}) {
// Increase score.s
game.state.points += 1;
254
} else {
match i {
4 => 6,
6 => 1,
1 => 3,
3 => 2,
2 => 7,
7 => 5,
5 => 4,
_ => unreachable!(),
}
};
*tiletypeid = NonZeroU8::try_from(tilenum).unwrap();
}
}
}
// Adjust 'camera' if needed.
let has_hit_camera_top =
Game::LOCK_OUT_HEIGHT - Self::CAMERA_MARGIN_TOP <= (piece.position.1 as usize);
if !has_hit_camera_top {
return;
}
// Ascending virtual infinite board.
let mut ascent_lines =
Self::prng_ascent_lines(&mut self.height_loaded, &mut game.state.rng);
game.state.board.rotate_left(1);
game.state.board[Game::HEIGHT - 1] = ascent_lines.next().unwrap();
piece.position.1 -= 1;
// Count height in game state.
game.state.lineclears += 1;
}
// The mod must pre-process: 'hold' to replace with custom hold, and 'drops' to prevent piece locking.
fn on_player_input_received(
&mut self,
game: GameAccess,
_feed: &mut NotificationFeed,
_time: &mut InGameTime,
player_input: &mut Option<Input>,
) {
match player_input {
Some(Input::Activate(Button::HoldPiece)) => {
// Remove hold input to stop engine from processing it.
player_input.take();
// Manually swap pieces if available.
let (Some(piece), Some((held_tetromino, _))) =
(game.phase.piece_mut(), game.state.piece_held.as_mut())
else {
return;
};
(piece.tetromino, *held_tetromino) = (*held_tetromino, piece.tetromino);
}
Some(Input::Activate(Button::DropSoft | Button::DropHard)) => {
// Remove drop inputs to stop engine from locking down the piece.
player_input.take();
}
_ => {}
}
}
}
impl Ascent {
// Playable width needs to be odd.
const PLAYABLE_WIDTH: usize = Game::WIDTH - (1 - Game::WIDTH % 2);
// FIXME: Consider reintroducing: const CAMERA_ADJUST_DELAY: Duration = Duration::from_millis(125);
const CAMERA_MARGIN_TOP: usize = 5;
fn prng_ascent_lines<'a>(
height_loaded: &'a mut usize,
rng: &'a mut GameRng,
) -> impl Iterator<Item = Line> + 'a {
std::iter::repeat(Line::default()).map(|mut line| {
// Only generate the particular ascent line consisting of mino hinges if it's on an 'odd' height.
if !height_loaded.is_multiple_of(2) {
// Add hinges.
for (j, tile) in line.iter_mut().enumerate() {
if j % 2 == 1 {
let white_tile = Some(NonZeroU8::try_from(255).unwrap());
*tile = white_tile;
}
}
// Add gem.
let gem_idx = rng.random_range(0..Self::PLAYABLE_WIDTH);
if line[gem_idx].is_some() {
line[gem_idx] = Some(NonZeroU8::try_from(rng.random_range(1..=7)).unwrap());
}
}
// Extra tile for even board width and odd playable width.
if Self::PLAYABLE_WIDTH != line.len() {
let color = if (*height_loaded / 10).is_multiple_of(2)
^ (height_loaded.is_multiple_of(10) || *height_loaded % 10 == 9)
{
255 /*white*/
} else {
2 /*sky*/
};
line[Self::PLAYABLE_WIDTH] = Some(NonZeroU8::try_from(color).unwrap());
}
*height_loaded += 1;
line
})
}
}