#!/usr/bin/env python3 # fork of https://github.com/SmartViking/MaTris by deinkoks # removed everythin but elemental gameplay, added multiplayer controlled via mqtt # most ugly thing: a mqtt client for every player due to callback limitations import pygame from pygame import Rect, Surface import random import os import paho.mqtt.client as mqtt from tetrominoes import list_of_tetrominoes, shape_str from tetrominoes import rotate from scores import load_score, write_score class GameOver(Exception): """Exception used for its control flow properties""" def get_sound(filename): return pygame.mixer.Sound(os.path.join(os.path.dirname(__file__), "resources", filename)) ENABLE_STDIO = True #dump game on stdio, not sure if works as intended. ENABLE_MQTT = True # for debugging w/o a broker ENABLE_SOUND = False ENABLE_KEYBOARD_CONTROLS = False # working for player1. mostly MQTT_TOPIC = "deckentetris/" PLAYER = 10 # player count, 1 columns per player BGCOLOR = (15, 15, 20) BORDERCOLOR = (140, 140, 140) GAMEOVER_COLOR = (140, 100, 100) BLOCKSIZE = 1 # set this to 1 if run on the pixeldecke BORDERWIDTH = 0 MATRIS_OFFSET = 0 # we have 8pixel columns with a height of 5*8px for every player # lets use one for the next block, but 2 lines are hidden MATRIX_WIDTH = 8 MATRIX_HEIGHT = (4)*8 LEFT_MARGIN = 0 WIDTH = (MATRIX_WIDTH*BLOCKSIZE + MATRIS_OFFSET*2 + LEFT_MARGIN) * PLAYER HEIGHT = (MATRIX_HEIGHT-2)*BLOCKSIZE + MATRIS_OFFSET*2 HEIGHT += BLOCKSIZE*8 # additional tile for preview above game HEIGHT += 2 # height fix for reasons TRICKY_CENTERX = WIDTH-(WIDTH-(MATRIS_OFFSET+BLOCKSIZE*MATRIX_WIDTH))/2 VISIBLE_MATRIX_HEIGHT = MATRIX_HEIGHT - 2 class Matris(object): def on_msg(self, client, userdata, message): payload = str(message.payload.decode("utf-8")) if payload == "drop": self.hard_drop() elif payload == "rotate": self.request_rotation() elif payload == "left": self.request_movement('left') elif payload == "right": self.request_movement('right') elif payload == "update": self.mqtt.publish(MQTT_TOPIC+str(offset)+"/current", payload=shape_str(self.current_tetromino.shape), qos=0, retain=False) self.mqtt.publish(MQTT_TOPIC+str(offset)+"/next", payload=shape_str(self.next_tetromino.shape), qos=0, retain=False) def __init__(self, offset): self.surface = screen.subsurface(Rect((MATRIX_WIDTH * BLOCKSIZE*offset, 8*BLOCKSIZE), (MATRIX_WIDTH * BLOCKSIZE, (MATRIX_HEIGHT-2) * BLOCKSIZE))) self.offset = offset self.matrix = dict() for y in range(MATRIX_HEIGHT): for x in range(MATRIX_WIDTH): self.matrix[(y,x)] = None """ `self.matrix` is the current state of the tetris board, that is, it records which squares are currently occupied. It does not include the falling tetromino. The information relating to the falling tetromino is managed by `self.set_tetrominoes` instead. When the falling tetromino "dies", it will be placed in `self.matrix`. """ self.next_tetromino = random.choice(list_of_tetrominoes) self.set_tetrominoes() self.tetromino_rotation = 0 self.downwards_timer = 0 self.base_downwards_speed = 0.4 # Move down every 400 ms self.movement_keys = {'left': 0, 'right': 0} self.movement_keys_speed = 0.05 self.movement_keys_timer = (-self.movement_keys_speed)*2 self.level = 1 self.score = 0 self.lines = 0 self.combo = 1 # Combo will increase when you clear lines with several tetrominos in a row self.paused = False self.highscore = load_score() self.played_highscorebeaten_sound = False self.levelup_sound = get_sound("levelup.wav") self.gameover_sound = get_sound("gameover.wav") self.linescleared_sound = get_sound("linecleared.wav") self.highscorebeaten_sound = get_sound("highscorebeaten.wav") if ENABLE_MQTT: self.mqtt = mqtt.Client() self.mqtt.connect("mqtt.chaospott.de", port=1883, keepalive=60, bind_address="") self.mqtt.subscribe(MQTT_TOPIC+str(offset)+"/action", 2) self.mqtt.on_message = self.on_msg self.mqtt.loop_start() self.mqtt.publish(MQTT_TOPIC+str(offset)+"/current", payload=shape_str(self.current_tetromino.shape), qos=0, retain=False) self.mqtt.publish(MQTT_TOPIC+str(offset)+"/next", payload=shape_str(self.next_tetromino.shape), qos=0, retain=False) def set_tetrominoes(self): self.current_tetromino = self.next_tetromino self.next_tetromino = random.choice(list_of_tetrominoes) self.surface_of_next_tetromino = self.construct_surface_of_next_tetromino() self.tetromino_position = (0,4) if len(self.current_tetromino.shape) == 2 else (0, 3) self.tetromino_rotation = 0 self.tetromino_block = self.block(self.current_tetromino.color) self.shadow_block = self.block(self.current_tetromino.color, shadow=True) def hard_drop(self): amount = 0 while self.request_movement('down'): amount += 1 self.score += 10*amount self.lock_tetromino() def update(self, timepassed): self.needs_redraw = False if ENABLE_KEYBOARD_CONTROLS: pressed = lambda key: event.type == pygame.KEYDOWN and event.key == key unpressed = lambda key: event.type == pygame.KEYUP and event.key == key events = pygame.event.get() for event in events: if pressed(pygame.K_p): self.surface.fill((0,0,0)) self.needs_redraw = True self.paused = not self.paused elif event.type == pygame.QUIT: self.gameover(full_exit=True) elif pressed(pygame.K_ESCAPE): self.gameover() if self.paused: return self.needs_redraw if ENABLE_KEYBOARD_CONTROLS: for event in events: if pressed(pygame.K_SPACE): self.hard_drop() elif pressed(pygame.K_UP) or pressed(pygame.K_w): self.request_rotation() elif pressed(pygame.K_LEFT) or pressed(pygame.K_a): self.request_movement('left') self.movement_keys['left'] = 1 elif pressed(pygame.K_RIGHT) or pressed(pygame.K_d): self.request_movement('right') self.movement_keys['right'] = 1 elif unpressed(pygame.K_LEFT) or unpressed(pygame.K_a): self.movement_keys['left'] = 0 self.movement_keys_timer = (-self.movement_keys_speed)*2 elif unpressed(pygame.K_RIGHT) or unpressed(pygame.K_d): self.movement_keys['right'] = 0 self.movement_keys_timer = (-self.movement_keys_speed)*2 self.downwards_speed = self.base_downwards_speed ** (1 + self.level/10.) self.downwards_timer += timepassed if ENABLE_KEYBOARD_CONTROLS: downwards_speed = self.downwards_speed*0.10 if any([pygame.key.get_pressed()[pygame.K_DOWN], pygame.key.get_pressed()[pygame.K_s]]) else self.downwards_speed else: downwards_speed = self.downwards_speed if self.downwards_timer > downwards_speed: if not self.request_movement('down'): self.lock_tetromino() self.downwards_timer %= downwards_speed if ENABLE_KEYBOARD_CONTROLS: if any(self.movement_keys.values()): self.movement_keys_timer += timepassed if self.movement_keys_timer > self.movement_keys_speed: self.request_movement('right' if self.movement_keys['right'] else 'left') self.movement_keys_timer %= self.movement_keys_speed return self.needs_redraw def draw_surface(self): with_tetromino = self.blend(matrix=self.place_shadow()) for y in range(MATRIX_HEIGHT): for x in range(MATRIX_WIDTH): # I hide the 2 first rows by drawing them outside of the surface block_location = Rect(x*BLOCKSIZE, (y*BLOCKSIZE - 2*BLOCKSIZE), BLOCKSIZE, BLOCKSIZE) if with_tetromino[(y,x)] is None: self.surface.fill(BGCOLOR, block_location) else: if with_tetromino[(y,x)][0] == 'shadow': self.surface.fill(BGCOLOR, block_location) self.surface.blit(with_tetromino[(y,x)][1], block_location) def gameover(self, full_exit=False): """ Gameover occurs when a new tetromino does not fit after the old one has died, either after a "natural" drop or a hard drop by the player. That is why `self.lock_tetromino` is responsible for checking if it's game over. """ #write_score(self.score) #if full_exit: # exit() #else: # raise GameOver("Sucker!") self.paused = True self.surface.fill(GAMEOVER_COLOR) self.needs_redraw =True def place_shadow(self): posY, posX = self.tetromino_position while self.blend(position=(posY, posX)): posY += 1 position = (posY-1, posX) return self.blend(position=position, shadow=True) def fits_in_matrix(self, shape, position): posY, posX = position for x in range(posX, posX+len(shape)): for y in range(posY, posY+len(shape)): if self.matrix.get((y, x), False) is False and shape[y-posY][x-posX]: # outside matrix return False return position def request_rotation(self): rotation = (self.tetromino_rotation + 1) % 4 shape = self.rotated(rotation) y, x = self.tetromino_position position = (self.fits_in_matrix(shape, (y, x)) or self.fits_in_matrix(shape, (y, x+1)) or self.fits_in_matrix(shape, (y, x-1)) or self.fits_in_matrix(shape, (y, x+2)) or self.fits_in_matrix(shape, (y, x-2))) # ^ That's how wall-kick is implemented if position and self.blend(shape, position): self.tetromino_rotation = rotation self.tetromino_position = position self.needs_redraw = True return self.tetromino_rotation else: return False def request_movement(self, direction): posY, posX = self.tetromino_position if direction == 'left' and self.blend(position=(posY, posX-1)): self.tetromino_position = (posY, posX-1) self.needs_redraw = True return self.tetromino_position elif direction == 'right' and self.blend(position=(posY, posX+1)): self.tetromino_position = (posY, posX+1) self.needs_redraw = True return self.tetromino_position elif direction == 'up' and self.blend(position=(posY-1, posX)): self.needs_redraw = True self.tetromino_position = (posY-1, posX) return self.tetromino_position elif direction == 'down' and self.blend(position=(posY+1, posX)): self.needs_redraw = True self.tetromino_position = (posY+1, posX) return self.tetromino_position else: return False def rotated(self, rotation=None): if rotation is None: rotation = self.tetromino_rotation return rotate(self.current_tetromino.shape, rotation) def block(self, color, shadow=False): colors = {'blue': (105, 105, 255), 'yellow': (225, 242, 41), 'pink': (242, 41, 195), 'green': (22, 181, 64), 'red': (204, 22, 22), 'orange': (245, 144, 12), 'cyan': (10, 255, 226)} if shadow: end = [90] # end is the alpha value else: end = [] # Adding this to the end will not change the array, thus no alpha value border = Surface((BLOCKSIZE, BLOCKSIZE), pygame.SRCALPHA, 32) border.fill(list(map(lambda c: c*0.5, colors[color])) + end) borderwidth = 0 box = Surface((BLOCKSIZE-borderwidth*2, BLOCKSIZE-borderwidth*2), pygame.SRCALPHA, 32) boxarr = pygame.PixelArray(box) for x in range(len(boxarr)): for y in range(len(boxarr)): boxarr[x][y] = tuple(list(map(lambda c: min(255, int(c*random.uniform(0.8, 1.2))), colors[color])) + end) del boxarr # deleting boxarr or else the box surface will be 'locked' or something like that and won't blit. border.blit(box, Rect(borderwidth, borderwidth, 0, 0)) return border def lock_tetromino(self): """ This method is called whenever the falling tetromino "dies". `self.matrix` is updated, the lines are counted and cleared, and a new tetromino is chosen. """ self.matrix = self.blend() lines_cleared = self.remove_lines() self.lines += lines_cleared if lines_cleared: if lines_cleared >= 4 and ENABLE_SOUND: self.linescleared_sound.play() self.score += 100 * (lines_cleared**2) * self.combo if not self.played_highscorebeaten_sound and self.score > self.highscore: if self.highscore != 0 and ENABLE_SOUND: self.highscorebeaten_sound.play() self.played_highscorebeaten_sound = True if self.lines >= self.level*10: if ENABLE_SOUND: self.levelup_sound.play() self.level += 1 self.combo = self.combo + 1 if lines_cleared else 1 self.set_tetrominoes() if not self.blend(): if ENABLE_SOUND: self.gameover_sound.play() self.gameover() self.needs_redraw = True def remove_lines(self): lines = [] for y in range(MATRIX_HEIGHT): line = (y, []) for x in range(MATRIX_WIDTH): if self.matrix[(y,x)]: line[1].append(x) if len(line[1]) == MATRIX_WIDTH: lines.append(y) for line in sorted(lines): for x in range(MATRIX_WIDTH): self.matrix[(line,x)] = None for y in range(0, line+1)[::-1]: for x in range(MATRIX_WIDTH): self.matrix[(y,x)] = self.matrix.get((y-1,x), None) return len(lines) def blend(self, shape=None, position=None, matrix=None, shadow=False): """ Does `shape` at `position` fit in `matrix`? If so, return a new copy of `matrix` where all the squares of `shape` have been placed in `matrix`. Otherwise, return False. This method is often used simply as a test, for example to see if an action by the player is valid. It is also used in `self.draw_surface` to paint the falling tetromino and its shadow on the screen. """ if shape is None: shape = self.rotated() if position is None: position = self.tetromino_position copy = dict(self.matrix if matrix is None else matrix) posY, posX = position for x in range(posX, posX+len(shape)): for y in range(posY, posY+len(shape)): if (copy.get((y, x), False) is False and shape[y-posY][x-posX] # shape is outside the matrix or # coordinate is occupied by something else which isn't a shadow copy.get((y,x)) and shape[y-posY][x-posX] and copy[(y,x)][0] != 'shadow'): return False # Blend failed; `shape` at `position` breaks the matrix elif shape[y-posY][x-posX]: copy[(y,x)] = ('shadow', self.shadow_block) if shadow else ('block', self.tetromino_block) return copy def construct_surface_of_next_tetromino(self): shape = self.next_tetromino.shape surf = Surface((len(shape)*BLOCKSIZE, len(shape)*BLOCKSIZE), pygame.SRCALPHA, 32) for y in range(len(shape)): for x in range(len(shape)): if shape[y][x]: surf.blit(self.block(self.next_tetromino.color), (x*BLOCKSIZE, y*BLOCKSIZE)) return surf class Game(object): def main(self, screen): clock = pygame.time.Clock() self.games = [] for game in range(0,PLAYER): self.games.append(Matris(game)) #screen.blit(construct_nightmare(screen.get_size()), (0,0)) matris_border = Surface((MATRIX_WIDTH*BLOCKSIZE, VISIBLE_MATRIX_HEIGHT*BLOCKSIZE)) matris_border.fill(BORDERCOLOR) screen.blit(matris_border, (8*MATRIX_WIDTH*BLOCKSIZE*game,8*BLOCKSIZE)) self.redraw(self.games[-1]) pygame.draw.line(screen, (255, 255, 255), [0, 8*BLOCKSIZE-1], [MATRIX_WIDTH*BLOCKSIZE*PLAYER, 8*BLOCKSIZE-1], 1) while True: # MQTTC.loop() try: timepassed = clock.tick(50) for game in self.games: if game.update(timepassed / 1000.): self.redraw(game) except GameOver: return def redraw(self, matris): if not matris.paused: self.blit_next_tetromino(matris.surface_of_next_tetromino, matris.offset) # self.blit_info() matris.draw_surface() pygame.display.flip() if ENABLE_STDIO: #print("-------------") #print(pygame.image.tostring(screen, "RGB", False)) os.write(1, pygame.image.tostring(screen, "RGB", False)) def blit_next_tetromino(self, tetromino_surf, offset): area = Surface((BLOCKSIZE*5, BLOCKSIZE*5)) area.fill(BORDERCOLOR) area.fill(BGCOLOR, Rect(BORDERWIDTH, BORDERWIDTH, BLOCKSIZE*5-BORDERWIDTH*2, BLOCKSIZE*5-BORDERWIDTH*2)) areasize = area.get_size()[0] tetromino_surf_size = tetromino_surf.get_size()[0] # ^^ I'm assuming width and height are the same center = areasize/2 - tetromino_surf_size/2 area.blit(tetromino_surf, (center, center)) screen.blit(area, area.get_rect(top=0, centerx=(MATRIX_WIDTH*BLOCKSIZE*offset)+BLOCKSIZE*3)) def construct_highscoresurf(self): font = pygame.font.Font(None, 50) highscore = load_score() text = "Highscore: {}".format(highscore) return font.render(text, True, (255,255,255)) def construct_nightmare(size): #nightmare = background surf = Surface(size) boxsize = 8 bordersize = 1 vals = '1235' # only the lower values, for darker colors and greater fear arr = pygame.PixelArray(surf) for x in range(0, len(arr), boxsize): for y in range(0, len(arr[x]), boxsize): color = int(''.join([random.choice(vals) + random.choice(vals) for _ in range(3)]), 16) for LX in range(x, x+(boxsize - bordersize)): for LY in range(y, y+(boxsize - bordersize)): if LX < len(arr) and LY < len(arr[x]): arr[LX][LY] = color del arr return surf if __name__ == '__main__': pygame.init() print("Width:", WIDTH, "Height:", HEIGHT, "Playercount:", PLAYER) if ENABLE_MQTT or True: print("MQTT enabled:") print("TOPIC:", MQTT_TOPIC+"/action", " PAYLOAD: left|right|rotate|drop|update") print("TOPIC:", MQTT_TOPIC+"/current", "Show the current tetromino. Updated by update @ /action.") print("TOPIC:", MQTT_TOPIC+"/next", "Show the current tetromino. Updated by update @ /action.") screen = pygame.display.set_mode((WIDTH, HEIGHT)) pygame.display.set_caption("Deckentetris") # Menu().main(screen) Game().main(screen)