Hot-keys on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1from os.path import Path
2from fiber import sleep
3from random.pseudo import randint
4from sdl import Color
5from sdl import Font
6from sdl import KeyCode
7from sdl import KeyDownEvent
8from sdl import KeySym
9from sdl import Music
10from sdl import QuitEvent
11from sdl import Rect
12from sdl import Renderer
13from sdl import Surface
14from sdl import Texture
15from sdl import Wav
16from sdl import Window
17from sdl import create_rgb_surface
18from sdl import create_window_and_renderer
19from sdl import get_performance_counter
20from sdl import get_performance_frequency
21from sdl import img_init
22from sdl import img_load
23from sdl import mix_init
24from sdl import open_audio
25from sdl import poll_event
26from sdl import sdl_init
27from sdl import ttf_init
28from sdl import volume_music
30TITLE: string = "Mystris -- tribute to venerable Twintris"
32BLOCK_SIZE: i64 = 20
34TETRO_SIZE: i64 = 4
36FIELD_HEIGHT: i64 = 20
38FIELD_WIDTH: i64 = 10
40WIN_WIDTH: i64 = BLOCK_SIZE * FIELD_WIDTH * 3
42WIN_HEIGHT: i64 = BLOCK_SIZE * FIELD_HEIGHT
44MYS_LOGO_PATH: Path = Path(f"{__assets__}/images/mys-logo-30-25.png")
46FONT_PATH: Path = Path(f"{__assets__}/fonts/RobotoMono-Regular.ttf")
48FONT_SIZE: i64 = 24
50MUSIC_PATH: Path = Path(f"{__assets__}/sounds/TwintrisThosenine.mod")
52DROP_BLOCK_SOUND_PATH: Path = Path(f"{__assets__}/sounds/block.wav")
54CLEAR_SOUND_PATH: Path = Path(f"{__assets__}/sounds/clear.wav")
56AUDIO_BUFFER_SIZE: i64 = 1024
58BACKGROUND_COLOR: Color = Color(0, 0, 0, 0)
60FOREGROUND_COLOR: Color = Color(0, 170, 170, 0)
62TETRO_COLORS: [Color] = [
63 Color(0, 98, 192, 0),
64 Color(202, 125, 95, 0),
65 Color(0, 193, 191, 0),
66 Color(0, 193, 0, 0),
67 Color(191, 190, 0, 0),
68 Color(209, 0, 191, 0),
69 Color(209, 0, 0, 0)
70]
72TEXT_COLOR: Color = Color(255, 255, 255, 0)
74enum State:
75 Running
76 GameOver
78class NesRandom:
79 _previous_piece: i64
81 func random(self) -> i64:
82 piece = randint(0, 6)
84 if piece == self._previous_piece:
85 piece = randint(0, 6)
87 self._previous_piece = piece
89 return piece
91class FrameTiming:
92 ticks_per_frame: u64
93 start_tick: u64
95 func __init__(self):
96 self.ticks_per_frame = get_performance_frequency() / 30
98 func start(self):
99 self.start_tick = get_performance_counter()
101 func sleep_until_next_frame(self):
102 frame_ticks = get_performance_counter() - self.start_tick
104 if frame_ticks < self.ticks_per_frame:
105 sleep_ticks = self.ticks_per_frame - frame_ticks
106 sleep_time = f64(sleep_ticks) / f64(get_performance_frequency())
107 sleep(sleep_time)
109 self.start_tick = get_performance_counter()
111class Block:
112 x: i64
113 y: i64
115class Tetro:
116 color_index: i64
117 blocks: [[Block]]
119TETROS: [Tetro] = [
120 # xxx
121 # x
122 Tetro(0,
123 [
124 [Block(2, 1), Block(3, 1), Block(4, 1), Block(3, 2)],
125 [Block(3, 0), Block(2, 1), Block(3, 1), Block(3, 2)],
126 [Block(2, 1), Block(3, 1), Block(4, 1), Block(3, 0)],
127 [Block(3, 0), Block(3, 1), Block(4, 1), Block(3, 2)]
128 ]),
129 # xxx
130 # x
131 Tetro(1,
132 [
133 [Block(2, 1), Block(3, 1), Block(4, 1), Block(4, 2)],
134 [Block(3, 0), Block(3, 1), Block(2, 2), Block(3, 2)],
135 [Block(2, 0), Block(2, 1), Block(3, 1), Block(4, 1)],
136 [Block(3, 0), Block(4, 0), Block(3, 1), Block(3, 2)]
137 ]),
138 # xx
139 # xx
140 Tetro(2,
141 [
142 [Block(2, 1), Block(3, 1), Block(3, 2), Block(4, 2)],
143 [Block(4, 0), Block(3, 1), Block(4, 1), Block(3, 2)],
144 [Block(2, 1), Block(3, 1), Block(3, 2), Block(4, 2)],
145 [Block(4, 0), Block(3, 1), Block(4, 1), Block(3, 2)]
146 ]),
147 # xx
148 # xx
149 Tetro(3,
150 [
151 [Block(2, 1), Block(3, 1), Block(2, 2), Block(3, 2)],
152 [Block(2, 1), Block(3, 1), Block(2, 2), Block(3, 2)],
153 [Block(2, 1), Block(3, 1), Block(2, 2), Block(3, 2)],
154 [Block(2, 1), Block(3, 1), Block(2, 2), Block(3, 2)]
155 ]),
156 # xx
157 # xx
158 Tetro(4,
159 [
160 [Block(3, 1), Block(4, 1), Block(2, 2), Block(3, 2)],
161 [Block(3, 0), Block(3, 1), Block(4, 1), Block(4, 2)],
162 [Block(3, 1), Block(4, 1), Block(2, 2), Block(3, 2)],
163 [Block(3, 0), Block(3, 1), Block(4, 1), Block(4, 2)]
164 ]),
165 # xxx
166 # x
167 Tetro(5,
168 [
169 [Block(2, 1), Block(3, 1), Block(4, 1), Block(2, 2)],
170 [Block(2, 0), Block(3, 0), Block(3, 1), Block(3, 2)],
171 [Block(4, 0), Block(2, 1), Block(3, 1), Block(4, 1)],
172 [Block(3, 0), Block(3, 1), Block(3, 2), Block(4, 2)]
173 ]),
174 # xxxx
175 Tetro(6,
176 [
177 [Block(1, 1), Block(2, 1), Block(3, 1), Block(4, 1)],
178 [Block(3, -1), Block(3, 0), Block(3, 1), Block(3, 2)],
179 [Block(1, 1), Block(2, 1), Block(3, 1), Block(4, 1)],
180 [Block(3, -1), Block(3, 0), Block(3, 1), Block(3, 2)]
181 ])
182]
184func create_empty_row() -> [i64]:
185 row = [0 for _ in range(FIELD_WIDTH + 2)]
186 row[0] = -1
187 row[-1] = -1
189 return row
191func create_empty_field() -> [[i64]]:
192 field = [create_empty_row() for _ in range(FIELD_HEIGHT + 4)]
194 for i in range(FIELD_WIDTH + 2):
195 field[0][i] = -1
196 field[-1][i] = -1
198 return field
200class Game:
201 width: i64
202 height: i64
203 screen: Surface
204 texture: Texture
205 mys_logo: Surface
206 mys_logo_texture: Texture
207 window: Window
208 renderer: Renderer
209 tetro_stats: [i64]
210 key_fire: KeyCode
211 state: State
212 nes_random: NesRandom
213 font: Font
214 # field[y][x] contains the color of the block with (x,y) coordinates
215 # "-1" border is to avoid bounds checking.
216 # -1 -1 -1 -1
217 # -1 0 0 -1
218 # -1 0 0 -1
219 # -1 -1 -1 -1
220 field: [[i64]]
221 # X offset of the game display
222 x_offset: i64
223 tetro: Tetro
224 next_tetro_index: i64
225 x_pos: i64
226 y_pos: i64
227 rotation_index: i64
228 music: Music
229 drop_block_sound: Wav
230 clear_sound: Wav
232 func __init__(self, width: i64, height: i64, title: string):
233 self.width = width
234 self.height = height
235 self.tetro_stats = [0, 0, 0, 0, 0, 0, 0]
236 self.window, self.renderer = create_window_and_renderer(width, height, 0)
237 self.window.set_title(title)
238 self.screen = create_rgb_surface(0,
239 width,
240 height,
241 32,
242 0x00ff0000,
243 0x0000ff00,
244 0x000000ff,
245 0xff000000)
246 self.texture = self.renderer.create_texture(width, height)
247 self.setup_sound()
248 self.mys_logo = img_load(MYS_LOGO_PATH)
249 self.mys_logo_texture = self.renderer.create_texture_from_surface(self.mys_logo)
250 self.key_fire = KeyCode.L
251 self.state = State.GameOver
252 self.nes_random = NesRandom()
253 self.font = Font(FONT_PATH, FONT_SIZE)
254 self.x_offset = FIELD_WIDTH * BLOCK_SIZE + 1
255 self.next_tetro_index = 0
256 self.field = create_empty_field()
258 func restart(self):
259 self.tetro_stats = [0, 0, 0, 0, 0, 0, 0]
260 self.state = State.Running
261 self.x_offset = FIELD_WIDTH * BLOCK_SIZE + 1
262 self.next_tetro_index = self.nes_random.random()
263 self.new_tetro()
264 self.field = create_empty_field()
266 func setup_sound(self):
267 open_audio(48000, 2, AUDIO_BUFFER_SIZE)
268 volume_music()
269 self.music = Music(MUSIC_PATH)
270 self.music.play(1)
271 self.drop_block_sound = Wav(DROP_BLOCK_SOUND_PATH)
272 self.clear_sound = Wav(CLEAR_SOUND_PATH)
274 func run(self):
275 frame_timing = FrameTiming()
276 frame_timing.start()
277 frame_counter = 0
279 while True:
280 if self.state == State.GameOver:
281 self.restart()
283 frame_counter += 1
285 if frame_counter % 30 == 0:
286 self.move_tetro_down()
288 self.draw()
290 if not self.handle_events():
291 break
293 frame_timing.sleep_until_next_frame()
295 func draw(self):
296 self.renderer.clear()
297 self.draw_background()
298 self.draw_tetro()
299 self.draw_field()
300 self.draw_vertial_splitters()
301 self.draw_stats_blocks()
302 self.draw_next_tetro()
303 self.draw_middle()
304 self.draw_logo()
305 self.draw_stats_text()
306 self.draw_next_tetro_text()
307 self.renderer.present()
309 func draw_background(self):
310 self.fill_rect(Rect(0, 0, self.width, self.height), BACKGROUND_COLOR)
312 func draw_tetro(self):
313 for block in self.tetro.blocks[self.rotation_index]:
314 self.draw_block(self.x_pos + block.x,
315 self.y_pos + block.y,
316 self.tetro.color_index,
317 self.x_offset)
319 func draw_field(self):
320 for y in range(3, FIELD_HEIGHT + 3):
321 for x in range(1, FIELD_WIDTH + 1):
322 row = self.field[y]
324 if row[x] > 0:
325 self.draw_block(x, y, row[x] - 1, self.x_offset)
327 func draw_block(self, x: i64, y: i64, color_index: i64, x_offset: i64):
328 rect = Rect(x_offset + (x - 1) * BLOCK_SIZE,
329 (y - 3) * BLOCK_SIZE,
330 BLOCK_SIZE - 1,
331 BLOCK_SIZE - 1)
332 self.fill_rect(rect, TETRO_COLORS[color_index])
334 func draw_vertial_splitters(self):
335 rect = Rect(BLOCK_SIZE * FIELD_WIDTH - 2, 0, 2, self.height)
336 self.fill_rect(rect, FOREGROUND_COLOR)
337 rect.x = WIN_WIDTH - BLOCK_SIZE * FIELD_WIDTH + 1
338 self.fill_rect(rect, FOREGROUND_COLOR)
340 func draw_stats_blocks(self):
341 for i, stat in enumerate(self.tetro_stats):
342 tetro = TETROS[i]
344 for block in tetro.blocks[0]:
345 x = (block.x + 1) * BLOCK_SIZE
346 y = i * (5 * BLOCK_SIZE / 2) + (block.y + 1) * BLOCK_SIZE
347 rect = Rect(x, y, BLOCK_SIZE - 1, BLOCK_SIZE - 1)
348 self.fill_rect(rect, TETRO_COLORS[tetro.color_index])
350 func draw_stats_text(self):
351 self.draw_text(BLOCK_SIZE, 0, "STATISTICS", TEXT_COLOR)
353 for i, stat in enumerate(self.tetro_stats):
354 y = (i + 1) * (5 * BLOCK_SIZE / 2) - FONT_SIZE / 2
355 self.draw_text(BLOCK_SIZE * 7, y, str(self.tetro_stats[i]), TEXT_COLOR)
357 func draw_next_tetro(self):
358 tetro = TETROS[self.next_tetro_index]
360 for block in tetro.blocks[0]:
361 x = (block.x + 22) * BLOCK_SIZE
362 y = (5 * BLOCK_SIZE / 2) + (block.y + 6) * BLOCK_SIZE
363 rect = Rect(x, y, BLOCK_SIZE - 1, BLOCK_SIZE - 1)
364 self.fill_rect(rect, TETRO_COLORS[tetro.color_index])
366 func draw_next_tetro_text(self):
367 self.draw_text(BLOCK_SIZE * 23, BLOCK_SIZE * 7, "NEXT", TEXT_COLOR)
369 func draw_middle(self):
370 self.texture.update(self.screen)
371 self.renderer.copy(self.texture, None)
373 func draw_logo(self):
374 tw, th = self.mys_logo_texture.query()
375 self.renderer.copy(self.mys_logo_texture,
376 Rect(5 * (WIN_WIDTH - tw / 2) / 6, 20, tw, th))
378 func draw_text(self, x: i64, y: i64, text: string, color: Color):
379 surface = self.font.render_solid(text, color)
380 texture = self.renderer.create_texture_from_surface(surface)
381 width, height = texture.query()
382 self.renderer.copy(texture, Rect(x, y, width, height))
384 func handle_events(self) -> bool:
385 running = True
387 while True:
388 match poll_event():
389 case QuitEvent():
390 running = False
391 case KeyDownEvent() as event:
392 self.handle_key(event.keysym.sym)
393 case _:
394 break
396 return running
398 func handle_key(self, key: KeyCode):
399 if key == KeyCode.Space:
400 self.handle_key_space()
401 elif key == self.key_fire:
402 self.handle_key_fire()
404 if self.state != State.Running:
405 return
407 if key == KeyCode.Z:
408 self.rotate_tetro(-1)
409 elif key == KeyCode.X:
410 self.rotate_tetro(1)
411 elif key == KeyCode.Left:
412 self.move_tetro_left()
413 elif key == KeyCode.Right:
414 self.move_tetro_right()
415 elif key == KeyCode.Down:
416 self.move_tetro_down()
418 func handle_key_space(self):
419 pass
421 func handle_key_fire(self):
422 pass
424 func rotate_tetro(self, step: i64):
425 rotation_index = self.rotation_index
426 self.rotation_index += step
427 self.rotation_index %= TETRO_SIZE
429 if not self.move_tetro_side(0):
430 self.rotation_index = rotation_index
432 func move_tetro_left(self):
433 self.move_tetro_side(-1)
435 func move_tetro_right(self):
436 self.move_tetro_side(1)
438 func move_tetro_side(self, dx: i64) -> bool:
439 # Reached left/right edge or another tetro?
440 for block in self.tetro.blocks[self.rotation_index]:
441 x = block.x + self.x_pos + dx
442 row = self.field[block.y + self.y_pos]
444 if row[x] != 0:
445 # Do not move
446 return False
448 self.x_pos += dx
450 return True
452 func move_tetro_down(self):
453 for block in self.tetro.blocks[self.rotation_index]:
454 y = block.y + self.y_pos + 1
455 x = block.x + self.x_pos
456 # Reached the bottom of the screen or another block?
457 row = self.field[y]
459 if row[x] != 0:
460 if self.y_pos < 3:
461 # The new tetro has no space to drop => end of the game
462 self.state = State.GameOver
463 else:
464 # Drop it and generate a new one
465 self.drop_tetro()
466 self.new_tetro()
468 if self.delete_cleared_lines():
469 self.clear_sound.play(0, 0)
470 else:
471 self.drop_block_sound.play(0, 0)
473 return
475 self.y_pos += 1
477 func new_tetro(self):
478 self.x_pos = FIELD_WIDTH / 2 - TETRO_SIZE / 2
479 self.y_pos = 2
480 self.tetro = TETROS[self.next_tetro_index]
481 self.rotation_index = 0
482 self.tetro_stats[self.next_tetro_index] += 1
483 self.next_tetro_index = self.nes_random.random()
485 func drop_tetro(self):
486 for block in self.tetro.blocks[self.rotation_index]:
487 x = block.x + self.x_pos
488 y = block.y + self.y_pos
489 row = self.field[y]
490 row[x] = self.tetro.color_index + 1
492 func fill_rect(self, rect: Rect, color: Color):
493 value = self.screen.map_rgb(color.r, color.g, color.b)
494 self.screen.fill_rect(rect, value)
496 func delete_cleared_lines(self) -> bool:
497 number_of_cleared_lines = 0
499 for y in range(2, FIELD_HEIGHT + 3):
500 if self.delete_cleared_line(y):
501 number_of_cleared_lines += 1
503 return number_of_cleared_lines > 0
505 func delete_cleared_line(self, y: i64) -> bool:
506 for v in slice(self.field[y], 1, -1):
507 if v == 0:
508 return False
510 for yp in range(y, 2, -1):
511 self.field[yp] = self.field[yp - 1]
513 self.field[2] = create_empty_row()
515 return True
517func main():
518 sdl_init()
519 ttf_init()
520 img_init()
521 mix_init()
522 game = Game(WIN_WIDTH, WIN_HEIGHT, TITLE)
523 game.run()