pixelserver2/apps/deckentetris/deckentetris.py
deckensteuerung 6c7d81ed49 foo
2018-10-21 15:19:51 +02:00

533 lines
20 KiB
Python
Executable File

#!/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+"<playerid>/action", " PAYLOAD: left|right|rotate|drop|update")
print("TOPIC:", MQTT_TOPIC+"<playerid>/current", "Show the current tetromino. Updated by update @ /action.")
print("TOPIC:", MQTT_TOPIC+"<playerid>/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)