Hide keyboard shortcuts

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 

29 

30TITLE: string = "Mystris -- tribute to venerable Twintris" 

31 

32BLOCK_SIZE: i64 = 20 

33 

34TETRO_SIZE: i64 = 4 

35 

36FIELD_HEIGHT: i64 = 20 

37 

38FIELD_WIDTH: i64 = 10 

39 

40WIN_WIDTH: i64 = BLOCK_SIZE * FIELD_WIDTH * 3 

41 

42WIN_HEIGHT: i64 = BLOCK_SIZE * FIELD_HEIGHT 

43 

44MYS_LOGO_PATH: Path = Path(f"{__assets__}/images/mys-logo-30-25.png") 

45 

46FONT_PATH: Path = Path(f"{__assets__}/fonts/RobotoMono-Regular.ttf") 

47 

48FONT_SIZE: i64 = 24 

49 

50MUSIC_PATH: Path = Path(f"{__assets__}/sounds/TwintrisThosenine.mod") 

51 

52DROP_BLOCK_SOUND_PATH: Path = Path(f"{__assets__}/sounds/block.wav") 

53 

54CLEAR_SOUND_PATH: Path = Path(f"{__assets__}/sounds/clear.wav") 

55 

56AUDIO_BUFFER_SIZE: i64 = 1024 

57 

58BACKGROUND_COLOR: Color = Color(0, 0, 0, 0) 

59 

60FOREGROUND_COLOR: Color = Color(0, 170, 170, 0) 

61 

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] 

71 

72TEXT_COLOR: Color = Color(255, 255, 255, 0) 

73 

74enum State: 

75 Running 

76 GameOver 

77 

78class NesRandom: 

79 _previous_piece: i64 

80 

81 func random(self) -> i64: 

82 piece = randint(0, 6) 

83 

84 if piece == self._previous_piece: 

85 piece = randint(0, 6) 

86 

87 self._previous_piece = piece 

88 

89 return piece 

90 

91class FrameTiming: 

92 ticks_per_frame: u64 

93 start_tick: u64 

94 

95 func __init__(self): 

96 self.ticks_per_frame = get_performance_frequency() / 30 

97 

98 func start(self): 

99 self.start_tick = get_performance_counter() 

100 

101 func sleep_until_next_frame(self): 

102 frame_ticks = get_performance_counter() - self.start_tick 

103 

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) 

108 

109 self.start_tick = get_performance_counter() 

110 

111class Block: 

112 x: i64 

113 y: i64 

114 

115class Tetro: 

116 color_index: i64 

117 blocks: [[Block]] 

118 

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] 

183 

184func create_empty_row() -> [i64]: 

185 row = [0 for _ in range(FIELD_WIDTH + 2)] 

186 row[0] = -1 

187 row[-1] = -1 

188 

189 return row 

190 

191func create_empty_field() -> [[i64]]: 

192 field = [create_empty_row() for _ in range(FIELD_HEIGHT + 4)] 

193 

194 for i in range(FIELD_WIDTH + 2): 

195 field[0][i] = -1 

196 field[-1][i] = -1 

197 

198 return field 

199 

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 

231 

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() 

257 

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() 

265 

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) 

273 

274 func run(self): 

275 frame_timing = FrameTiming() 

276 frame_timing.start() 

277 frame_counter = 0 

278 

279 while True: 

280 if self.state == State.GameOver: 

281 self.restart() 

282 

283 frame_counter += 1 

284 

285 if frame_counter % 30 == 0: 

286 self.move_tetro_down() 

287 

288 self.draw() 

289 

290 if not self.handle_events(): 

291 break 

292 

293 frame_timing.sleep_until_next_frame() 

294 

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() 

308 

309 func draw_background(self): 

310 self.fill_rect(Rect(0, 0, self.width, self.height), BACKGROUND_COLOR) 

311 

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) 

318 

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] 

323 

324 if row[x] > 0: 

325 self.draw_block(x, y, row[x] - 1, self.x_offset) 

326 

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]) 

333 

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) 

339 

340 func draw_stats_blocks(self): 

341 for i, stat in enumerate(self.tetro_stats): 

342 tetro = TETROS[i] 

343 

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]) 

349 

350 func draw_stats_text(self): 

351 self.draw_text(BLOCK_SIZE, 0, "STATISTICS", TEXT_COLOR) 

352 

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) 

356 

357 func draw_next_tetro(self): 

358 tetro = TETROS[self.next_tetro_index] 

359 

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]) 

365 

366 func draw_next_tetro_text(self): 

367 self.draw_text(BLOCK_SIZE * 23, BLOCK_SIZE * 7, "NEXT", TEXT_COLOR) 

368 

369 func draw_middle(self): 

370 self.texture.update(self.screen) 

371 self.renderer.copy(self.texture, None) 

372 

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)) 

377 

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)) 

383 

384 func handle_events(self) -> bool: 

385 running = True 

386 

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 

395 

396 return running 

397 

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() 

403 

404 if self.state != State.Running: 

405 return 

406 

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() 

417 

418 func handle_key_space(self): 

419 pass 

420 

421 func handle_key_fire(self): 

422 pass 

423 

424 func rotate_tetro(self, step: i64): 

425 rotation_index = self.rotation_index 

426 self.rotation_index += step 

427 self.rotation_index %= TETRO_SIZE 

428 

429 if not self.move_tetro_side(0): 

430 self.rotation_index = rotation_index 

431 

432 func move_tetro_left(self): 

433 self.move_tetro_side(-1) 

434 

435 func move_tetro_right(self): 

436 self.move_tetro_side(1) 

437 

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] 

443 

444 if row[x] != 0: 

445 # Do not move 

446 return False 

447 

448 self.x_pos += dx 

449 

450 return True 

451 

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] 

458 

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() 

467 

468 if self.delete_cleared_lines(): 

469 self.clear_sound.play(0, 0) 

470 else: 

471 self.drop_block_sound.play(0, 0) 

472 

473 return 

474 

475 self.y_pos += 1 

476 

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() 

484 

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 

491 

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) 

495 

496 func delete_cleared_lines(self) -> bool: 

497 number_of_cleared_lines = 0 

498 

499 for y in range(2, FIELD_HEIGHT + 3): 

500 if self.delete_cleared_line(y): 

501 number_of_cleared_lines += 1 

502 

503 return number_of_cleared_lines > 0 

504 

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 

509 

510 for yp in range(y, 2, -1): 

511 self.field[yp] = self.field[yp - 1] 

512 

513 self.field[2] = create_empty_row() 

514 

515 return True 

516 

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()