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 14KB

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