Python RGB Matrix games and animations https://www.xythobuz.de/ledmatrix_v2.html
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.

tetris.py 16KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480
  1. #!/usr/bin/env python3
  2. # ----------------------------------------------------------------------------
  3. # "THE BEER-WARE LICENSE" (Revision 42):
  4. # <xythobuz@xythobuz.de> wrote this file. As long as you retain this notice
  5. # you can do whatever you want with this stuff. If we meet some day, and you
  6. # think this stuff is worth it, you can buy me a beer in return. Thomas Buck
  7. # ----------------------------------------------------------------------------
  8. from scroll import ScrollText
  9. import time
  10. import random
  11. import util
  12. class Tetris:
  13. def __init__(self, g, i, ts = 0.5, to = 60.0, w = 10, h = 22, rc = False):
  14. self.gui = g
  15. self.input = i
  16. self.timestep = ts
  17. self.timeout = to
  18. self.width = min(w, self.gui.width)
  19. self.height = min(h, self.gui.height)
  20. self.randomizeColors = rc
  21. self.endText = ScrollText(self.gui, "Game Over!", "uushi",
  22. 1, 50, (251, 72, 196))
  23. self.scoreText = ScrollText(self.gui, "Score:", "uushi",
  24. 3, 50, (63, 255, 33))
  25. self.bg = (0, 0, 0)
  26. self.colors = [
  27. (251, 72, 196), # camp23 pink
  28. (63, 255, 33), # camp23 green
  29. (255, 0, 0), # red
  30. #(0, 255, 0), # green
  31. (0, 0, 255), # blue
  32. (255, 255, 0), # yellow
  33. (0, 255, 255), # cyan
  34. #(255, 0, 255),
  35. #(255, 255, 255),
  36. (255, 127, 0), # orange
  37. (127, 127, 127), # grey
  38. ]
  39. DrawText = util.getTextDrawer()
  40. self.text = []
  41. self.text.append(DrawText(self.gui, self.colors[1])) # Green, "Score"
  42. self.text.append(DrawText(self.gui, self.colors[0])) # Pink, Score Number
  43. self.text.append(DrawText(self.gui, self.colors[4])) # Yellow, "Paused"
  44. self.text.append(DrawText(self.gui, self.colors[5])) # Blue, "next"
  45. self.text.append(DrawText(self.gui, self.colors[0])) # Pink, "Tetris"
  46. self.text.append(DrawText(self.gui, self.colors[5])) # Blue, "up"
  47. self.text[0].setText("Score:", "tom-thumb")
  48. self.text[1].setText("0", "tom-thumb")
  49. self.text[2].setText("Paused", "tom-thumb")
  50. self.text[3].setText("next", "tom-thumb")
  51. if self.gui.height > self.gui.panelH:
  52. self.text[4].setText("Tetris", "ib8x8u")
  53. self.text[5].setText("up:", "tom-thumb")
  54. else:
  55. self.text[4].setText("Tetris", "tom-thumb")
  56. self.text[5].setText("up", "tom-thumb")
  57. # all [y][x] sub-lists must be the same length
  58. self.pieces = [
  59. # "I"
  60. [
  61. [1],
  62. [1],
  63. [1],
  64. [1],
  65. ],
  66. # "L"
  67. [
  68. [1, 0],
  69. [1, 0],
  70. [1, 1],
  71. ],
  72. # "J"
  73. [
  74. [0, 1],
  75. [0, 1],
  76. [1, 1],
  77. ],
  78. # "T"
  79. [
  80. [1, 1, 1],
  81. [0, 1, 0],
  82. ],
  83. # "O"
  84. [
  85. [1, 1],
  86. [1, 1],
  87. ],
  88. # "S"
  89. [
  90. [0, 1, 1],
  91. [1, 1, 0],
  92. ],
  93. # "Z"
  94. [
  95. [1, 1, 0],
  96. [0, 1, 1],
  97. ],
  98. ]
  99. self.max_width = 0
  100. self.max_height = 0
  101. for piece in self.pieces:
  102. if len(piece) > self.max_height:
  103. self.max_height = len(piece)
  104. if len(piece[0]) > self.max_width:
  105. self.max_width = len(piece[0])
  106. self.max_width += 2
  107. self.max_height += 2
  108. self.fact = 1
  109. self.x_off = 1
  110. self.y_off = self.gui.height - self.height - 3
  111. self.next_x_off = self.gui.panelW - self.max_width + 24
  112. self.next_y_off = self.gui.panelH - self.max_height - 3
  113. if self.gui.height > self.gui.panelH:
  114. self.fact = 2
  115. self.y_off = self.gui.height - self.height * self.fact - 3
  116. self.next_y_off = self.gui.panelH - self.max_height * self.fact + 29
  117. random.seed()
  118. self.restart()
  119. def restart(self):
  120. self.start = time.time()
  121. self.last = time.time()
  122. self.button = None
  123. self.score = 0
  124. self.text[1].setText(str(self.score), "tom-thumb")
  125. self.done = False
  126. self.data = [[self.bg for y in range(self.height)] for x in range(self.width)]
  127. self.piece = None
  128. self.next_piece = None
  129. self.old_keys = {
  130. "left": False,
  131. "right": False,
  132. "up": False,
  133. "down": False,
  134. "a": False,
  135. "b": False,
  136. "x": False,
  137. "y": False,
  138. "l": False,
  139. "r": False,
  140. "start": False,
  141. "select": False,
  142. }
  143. self.pause = False
  144. def finished(self):
  145. if self.input == None:
  146. # backup timeout for "AI"
  147. if (time.time() - self.start) >= self.timeout:
  148. return True
  149. if self.done:
  150. # game over screen
  151. return self.scoreText.finished()
  152. return False
  153. def collision(self):
  154. # check for collision of piece with data
  155. pos = (self.piece[2], self.piece[3])
  156. for y in range(0, len(self.piece[0])):
  157. for x in range(0, len(self.piece[0][y])):
  158. # only check where piece actually is
  159. if self.piece[0][y][x] == 0:
  160. continue
  161. # check for collision with bottom wall
  162. if (y + pos[1]) >= self.height:
  163. return True
  164. # check for collision with right wall
  165. if (x + pos[0]) >= self.width:
  166. return True
  167. # check for collision with previous pieces
  168. if self.data[x + pos[0]][y + pos[1]] != self.bg:
  169. return True
  170. return False
  171. # copy piece into data buffer
  172. def put(self, clear, pos = None, pie = None):
  173. position = pos
  174. if position == None:
  175. position = (self.piece[2], self.piece[3])
  176. piece = pie
  177. if piece == None:
  178. piece = self.piece[0]
  179. for y in range(0, len(piece)):
  180. for x in range(0, len(piece[y])):
  181. # only set or clear where piece actually is
  182. if piece[y][x] == 0:
  183. continue
  184. if clear:
  185. self.data[x + position[0]][y + position[1]] = self.bg
  186. else:
  187. self.data[x + position[0]][y + position[1]] = self.piece[1]
  188. def checkWin(self):
  189. had_data = False
  190. for y in range(0, self.height):
  191. line_full = True
  192. for x in range(0, self.width):
  193. if self.data[x][y] == self.bg:
  194. line_full = False
  195. else:
  196. had_data = True
  197. if had_data and line_full:
  198. self.score += 1
  199. self.text[1].setText(str(self.score), "tom-thumb")
  200. # move stuff above into this line
  201. for y2 in reversed(range(1, y + 1)):
  202. for x in range(0, self.width):
  203. self.data[x][y2] = self.data[x][y2 - 1]
  204. # clear out top line
  205. for x in range(0, self.width):
  206. self.data[x][0] = self.bg
  207. # check for complete win
  208. board_clear = (self.piece == None)
  209. for y in range(0, self.height):
  210. for x in range(0, self.width):
  211. if self.data[x][y] != self.bg:
  212. board_clear = False
  213. if board_clear == False:
  214. break
  215. if board_clear == False:
  216. break
  217. return board_clear
  218. def step(self):
  219. if self.next_piece == None:
  220. # select a new piece type and color
  221. p_i = random.randrange(0, len(self.pieces))
  222. if self.randomizeColors:
  223. c_i = random.randrange(0, len(self.colors))
  224. else:
  225. c_i = p_i % len(self.colors)
  226. self.next_piece = [
  227. self.pieces[p_i], # piece
  228. self.colors[c_i], # color
  229. 0, # x
  230. 0, # y
  231. ]
  232. # center the piece on top of the playing board
  233. self.next_piece[2] = int((self.width - len(self.next_piece[0][0])) / 2)
  234. # offsets for drawing the next piece
  235. self.piece_x_off = int((self.max_width - len(self.next_piece[0][0])) / 2)
  236. self.piece_y_off = int((self.max_height - len(self.next_piece[0])) / 2)
  237. if self.piece == None:
  238. justPlaced = True
  239. self.piece = self.next_piece
  240. self.next_piece = None
  241. if self.collision():
  242. # new piece immediately collided. game over!
  243. return False
  244. # copy piece into data buffer
  245. self.put(False)
  246. # don't move in the placement-step
  247. return True
  248. oldPosition = (self.piece[2], self.piece[3])
  249. oldPiece = [x[:] for x in self.piece[0]]
  250. # button input
  251. if self.button == "u":
  252. # rotate piece
  253. # https://stackoverflow.com/a/48444999
  254. self.piece[0] = [list(x) for x in zip(*self.piece[0][::-1])]
  255. elif self.button == "l":
  256. if self.piece[2] > 0:
  257. self.piece[2] -= 1
  258. elif self.button == "r":
  259. if self.piece[2] < (self.width - 1):
  260. self.piece[2] += 1
  261. else:
  262. # one pixel down
  263. self.piece[3] += 1
  264. # clear out piece from its old location
  265. self.put(True, oldPosition, oldPiece)
  266. collision = self.collision()
  267. if collision:
  268. # piece collided, put it back
  269. self.put(False, oldPosition, oldPiece)
  270. self.piece[0] = oldPiece
  271. self.piece[2] = oldPosition[0]
  272. self.piece[3] = oldPosition[1]
  273. if (self.button != "l") and (self.button != "r") and (self.button != "u"):
  274. # but only stop playing it if it was moving down
  275. self.piece = None
  276. # check for cleared line
  277. if self.checkWin():
  278. return False
  279. else:
  280. # copy piece at new location into buffer
  281. self.put(False)
  282. # clear previous input
  283. self.button = None
  284. return True
  285. def buttons(self):
  286. keys = self.input.get()
  287. if keys["left"] and (not self.old_keys["left"]) and (not self.old_keys["select"]):
  288. self.button = "l"
  289. elif keys["right"] and (not self.old_keys["right"]) and (not self.old_keys["select"]):
  290. self.button = "r"
  291. elif keys["up"] and (not self.old_keys["up"]) and (not self.old_keys["select"]):
  292. self.button = "u"
  293. elif keys["down"] and (not self.old_keys["select"]):
  294. self.button = "d"
  295. elif (keys["select"] and keys["start"] and (not self.old_keys["start"])) or (keys["start"] and keys["select"] and (not self.old_keys["select"])):
  296. self.restart()
  297. elif keys["start"] and (not self.old_keys["start"]) and (not self.old_keys["select"]):
  298. self.pause = not self.pause
  299. elif self.done and keys["start"] and (not self.old_keys["start"]):
  300. self.restart()
  301. elif self.done and keys["a"] and (not self.old_keys["a"]):
  302. self.restart()
  303. elif self.done and keys["b"] and (not self.old_keys["b"]):
  304. self.restart()
  305. elif self.done and keys["x"] and (not self.old_keys["x"]):
  306. self.restart()
  307. elif self.done and keys["y"] and (not self.old_keys["y"]):
  308. self.restart()
  309. self.old_keys = keys.copy()
  310. def draw_stats(self, off):
  311. x_off, y_off = off
  312. if self.fact > 1:
  313. self.text[0].draw(-x_off - 2, y_off - 6)
  314. self.text[1].draw(-x_off - 2, y_off)
  315. else:
  316. self.text[0].draw(-x_off - 2, y_off - 11)
  317. self.text[1].draw(-x_off - 2, y_off - 5)
  318. if self.pause:
  319. if self.fact > 1:
  320. self.text[2].draw(-x_off - 2 + 16, -y_off + 11 - 5)
  321. else:
  322. self.text[2].draw(-x_off - 2, -y_off + 11)
  323. def draw(self):
  324. if self.input != None:
  325. self.buttons()
  326. else:
  327. # TODO "AI"
  328. self.button = None
  329. if self.done:
  330. if self.endText.finished():
  331. self.scoreText.draw()
  332. else:
  333. self.endText.draw()
  334. self.scoreText.restart()
  335. return
  336. now = time.time()
  337. if (not self.pause) and ((self.button != None) or ((now - self.last) >= self.timestep) or (now < self.last)):
  338. # don't let user stop falling pieces by moving/rotating endlessly
  339. if (self.button != "l") and (self.button != "r") and (self.button != "u"):
  340. self.last = now
  341. cont = self.step()
  342. if cont == False:
  343. self.done = True
  344. self.scoreText.setText("Score: " + str(self.score), "uushi")
  345. self.endText.restart()
  346. # static text
  347. if self.fact > 1:
  348. self.text[4].draw(-2, -22)
  349. self.text[3].draw(-34, 13)
  350. self.text[5].draw(-34, 20)
  351. else:
  352. self.text[4].draw(-2, -11)
  353. self.text[3].draw(-14, 5)
  354. self.text[5].draw(-14, 12)
  355. # draw play area and border
  356. for x in range(-1, self.width + 1):
  357. for y in range(-1, self.height + 1):
  358. c = self.colors[7] # border color
  359. if (x >= 0) and (y >= 0) and (x < self.width) and (y < self.height):
  360. c = self.data[x][y]
  361. for x1 in range(0, self.fact):
  362. for y1 in range(0, self.fact):
  363. self.gui.set_pixel(
  364. self.fact * x + 1 + self.x_off + x1,
  365. self.fact * y + 1 + self.y_off + y1,
  366. c
  367. )
  368. # draw next piece and border
  369. for x in range(-1, self.max_width + 1):
  370. for y in range(-1, self.max_height + 1):
  371. c = self.colors[7] # border color
  372. if (x >= 0) and (y >= 0) and (x < self.max_width) and (y < self.max_height):
  373. if self.next_piece == None:
  374. c = (0, 0, 0)
  375. else:
  376. if (y >= self.piece_y_off) and (y < (len(self.next_piece[0]) + self.piece_y_off)) and (x >= self.piece_x_off) and (x < (len(self.next_piece[0][0]) + self.piece_x_off)):
  377. if self.next_piece[0][y - self.piece_y_off][x - self.piece_x_off] != 0:
  378. c = self.next_piece[1]
  379. else:
  380. c = (0, 0, 0)
  381. else:
  382. c = (0, 0, 0)
  383. for x1 in range(0, self.fact):
  384. for y1 in range(0, self.fact):
  385. self.gui.set_pixel(
  386. self.fact * x + 1 + self.next_x_off + x1,
  387. self.fact * y + 1 + self.next_y_off + y1,
  388. c
  389. )
  390. # find position for stats
  391. stats_off = None
  392. if self.gui.width > self.gui.panelW:
  393. stats_off = (self.gui.panelW, 0)
  394. elif self.gui.height > self.gui.panelH:
  395. stats_off = (0, self.gui.panelH)
  396. # second screen with stats
  397. if stats_off != None:
  398. self.draw_stats(stats_off)
  399. if __name__ == "__main__":
  400. # Need to import InputWrapper before initializing RGB Matrix on Pi
  401. i = util.getInput()
  402. t = util.getTarget(i)
  403. # show splash screen while initializing
  404. from splash import SplashScreen
  405. splash = SplashScreen(t)
  406. t.loop_start()
  407. splash.draw()
  408. t.loop_end()
  409. d = Tetris(t, i)
  410. util.loop(t, d.draw)