Toggle Navigation
Hatchery
Eggs
Tetris
__init__.py
Login
Register
__init__.py
Content
import buttons import color import display import leds import urandom import utime import vibra import easydraw urandom.seed(utime.time_ms()) # 160x80 display. we use it vertically. # 10 tetriminos per row -> 80 / 10 = 8 pixels per tetrimino TETRIMINO_SIZE = 8 FIELD_WIDTH = 10 FIELD_HEIGHT = 20 # Move the tetrimino down every x ticks. TETRIMINO_MOVE_DOWN_INTERVAL = 25 MAX_LEVEL = 11 LEVEL_SPEEDUP_STEP = 1 # Move left or right more often than down. TETRIMINO_MOVE_HORIZONTAL_INTERVAL = 10 TETRIMINO_NONE = 0 TETRIMINO_SQUARE = 1 TETRIMINO_T = 2 TETRIMINO_Z = 3 TETRIMINO_S = 4 TETRIMINO_J = 5 TETRIMINO_L = 6 TETRIMINO_I = 7 NUMBER_OF_TERIMINOS = 7 TETRIMINO_COLORS = [ (0, 0, 0), # None (255, 255, 0), # Square (153, 0, 255), # T (255, 0, 0), # Z (0, 255, 0), # S (0, 0, 255), # J (255, 165, 0), # L (0, 255, 255), # I ] ROTATION_0 = 0 ROTATION_90 = 1 ROTATION_180 = 2 ROTATION_270 = 3 TETRIMINO_GEOMETRY = [ (), # None ( # Square # == # == ( # The same in all rotations.. (1, 1), (1, 1), ), ( (1, 1), (1, 1), ), ( (1, 1), (1, 1), ), ( (1, 1), (1, 1), ), ), ( # T # === # = ( # ROTATION_0 (1, 1, 1), (0, 1, 0), ), # = # == # = ( # ROTATION_90 (0, 1), (1, 1), (0, 1), ), # = # === ( # ROTATION_180 (0, 1, 0), (1, 1, 1), ), # = # == # = ( # ROTATION_270 (1, 0), (1, 1), (1, 0), ), ), ( # Z # == # == ( # ROTATION_0 (1, 1, 0), (0, 1, 1), ), # = # == # = ( # ROTATION_90 (0, 1), (1, 1), (1, 0), ), # == # == ( # ROTATION_180 (1, 1, 0), (0, 1, 1), ), # = # == # = ( # ROTATION_270 (0, 1), (1, 1), (1, 0), ), ), ( # S # == # == ( # ROTATION_0 (0, 1, 1), (1, 1, 0), ), # = # == # = ( # ROTATION_90 (1, 0), (1, 1), (0, 1), ), # == # == ( # ROTATION_180 (0, 1, 1), (1, 1, 0), ), # = # == # = ( # ROTATION_270 (1, 0), (1, 1), (0, 1), ), ), ( # J # == # = # = ( # ROTATION_0 (1, 1), (1, 0), (1, 0), ), # === # = ( # ROTATION_90 (1, 1, 1), (0, 0, 1), ), # = # = # == ( # ROTATION_180 (0, 1), (0, 1), (1, 1), ), # = # === ( # ROTATION_270 (1, 0, 0), (1, 1, 1), ), ), ( # L # == # = # = ( # ROTATION_0 (1, 1), (0, 1), (0, 1), ), # = # === ( # ROTATION_90 (0, 0, 1), (1, 1, 1), ), # = # = # == ( # ROTATION_180 (1, 0), (1, 0), (1, 1), ), # === # = ( # ROTATION_270 (1, 1, 1), (1, 0, 0), ), ), ( # I # = # = # = # = ( # ROTATION_0 [1], [1], [1], [1], ), # ==== ( # ROTATION_90 (1, 1, 1, 1), ), # = # = # = # = ( # ROTATION_180 [1], [1], [1], [1], ), # ==== ( # ROTATION_270 (1, 1, 1, 1), ), ), ] def get_tetrimino_minmax(typ, rotation): if typ == TETRIMINO_SQUARE: return {'x': 2, 'y': 2} elif typ in [TETRIMINO_T, TETRIMINO_S, TETRIMINO_Z]: if rotation in [ROTATION_0, ROTATION_180]: return {'x': 3, 'y': 2} else: return {'x': 2, 'y': 3} elif typ in [TETRIMINO_L, TETRIMINO_J]: if rotation in [ROTATION_0, ROTATION_180]: return {'x': 2, 'y': 3} else: return {'x': 3, 'y': 2} elif typ == TETRIMINO_I: if rotation in [ROTATION_0, ROTATION_180]: return {'x': 1, 'y': 4} else: return {'x': 4, 'y': 1} return {'x': 0, 'y': 0} def get_tetrimino_default_rotation(typ): if typ in [TETRIMINO_SQUARE, TETRIMINO_S, TETRIMINO_Z]: return ROTATION_0 elif typ in [TETRIMINO_L, TETRIMINO_I]: return ROTATION_90 elif typ == TETRIMINO_T: return ROTATION_180 elif typ == TETRIMINO_J: return ROTATION_270 # def randombyte(min, max): # rand = os.urandom(2) # r = rand[0] | (rand[1] << 8) # | (rand[2] << 16) | (rand[3] << 24) # return math.ceil(float(r) / (float(32767) / float(max - min + 1))) + min - 1 class GameField: def __init__(self): self.gamefield = [[TETRIMINO_NONE] * FIELD_WIDTH for _ in range(FIELD_HEIGHT)] def field(self, x, y): return self.gamefield[y][x] def reset(self): for y in range(FIELD_HEIGHT): for x in range(FIELD_WIDTH): self.gamefield[y][x] = TETRIMINO_NONE def put_tetrimino(self, typ, rotation, position): shape = TETRIMINO_GEOMETRY[typ][rotation] for y in range(len(shape)): for x in range(len(shape[y])): # print(x, y, shape[y][x], y + position['y'], x + position['x']) if shape[y][x] == 1: # Draw it like it's shown in the defining code above. Not upside down. self.gamefield[((len(shape)-1) - y) + position['y']][x + position['x']] = typ def collides_with(self, other): for y in range(FIELD_HEIGHT): for x in range(FIELD_WIDTH): if self.gamefield[y][x] != TETRIMINO_NONE and other.gamefield[y][x] != TETRIMINO_NONE: return True return False def remove_row(self, row): self.gamefield = self.gamefield[:row] + self.gamefield[row+1:] self.gamefield.append([TETRIMINO_NONE] * FIELD_WIDTH) class Tetris: def __init__(self): self.new_game() def new_game(self): self.tick = 0 self.gameover = False self.down_interval = TETRIMINO_MOVE_DOWN_INTERVAL # Do we need to redraw the gamefield? self.dirty = True # gamefield[y][x] self.gamefield = GameField() self.temp_gamefield = GameField() self.current_tetrimino = 0 self.select_random_tetrimino() self.desired_direction = 0 # left = -1, none = 0, right = 1 self.desire_rotation = False self.rotation_button_pressed = False # Scoring self.lines_cleared = 0 self.score = 0 self.level = 1 self.combo = 0 # Reset the leds. self.update_leds() def loop(self): with display.open() as disp: while True: # main loop while not self.gameover: self.think(disp) utime.sleep_ms(10) # Start a new game on button press. b = 0 while b == 0: b = buttons.read(buttons.BOTTOM_LEFT | buttons.BOTTOM_RIGHT | buttons.TOP_RIGHT) utime.sleep_ms(50) self.new_game() def select_random_tetrimino(self): # randint doesn't seem to obey the range all the time. self.current_tetrimino = 0 while self.current_tetrimino <= 0 or self.current_tetrimino > NUMBER_OF_TERIMINOS: self.current_tetrimino = urandom.randint(1, NUMBER_OF_TERIMINOS) self.current_rotation = get_tetrimino_default_rotation(self.current_tetrimino) self.cursor = {} minmax = get_tetrimino_minmax(self.current_tetrimino, self.current_rotation) self.cursor['x'] = (FIELD_WIDTH // 2) - (minmax['x'] - 1) // 2 self.cursor['y'] = FIELD_HEIGHT - minmax['y'] def move_current_tetrimino_leftright(self): temp_cursor = {} temp_cursor['x'] = self.cursor['x'] temp_cursor['y'] = self.cursor['y'] # Player wants to move left. if self.desired_direction == -1: temp_cursor['x'] -= 1 else: temp_cursor['x'] += 1 # Player has to press the button again in the next time window. self.desired_direction = 0 # Still inside the gamefield? if temp_cursor['x'] < 0: return minmax = get_tetrimino_minmax(self.current_tetrimino, self.current_rotation) if temp_cursor['x'] + minmax['x'] - 1 >= FIELD_WIDTH: return # Something in the way? self.temp_gamefield.reset() self.temp_gamefield.put_tetrimino(self.current_tetrimino, self.current_rotation, temp_cursor) if self.temp_gamefield.collides_with(self.gamefield): return self.cursor['x'] = temp_cursor['x'] self.dirty = True def rotate_tetrimino(self): temp_rotation = (self.current_rotation + 1) % 4 temp_cursor = {} temp_cursor['x'] = self.cursor['x'] temp_cursor['y'] = self.cursor['y'] # Keep the cursor centered below the tetrimino. # based on the initial cursor position using minmax of tetrimino: # # Keep the center the same visually. # The tetrimino shape is always drawn in it's full 4x4 box, # so move the cursor in such a way, that the center # appears in the same square. # Like # =x= # c=o # # Becomes after rotation: # o=o # =xo # c=o # So move the cursor one up. if self.current_tetrimino == TETRIMINO_T: # Center: # =c= # = if temp_rotation == ROTATION_0: temp_cursor['x'] -= 1 elif temp_rotation == ROTATION_180: temp_cursor['y'] += 1 elif temp_rotation == ROTATION_270: temp_cursor['x'] += 1 temp_cursor['y'] -= 1 # == # c= elif self.current_tetrimino in [TETRIMINO_S, TETRIMINO_Z]: if temp_rotation in [ROTATION_0, ROTATION_180]: temp_cursor['x'] -= 1 temp_cursor['y'] += 1 elif temp_rotation in [ROTATION_90, ROTATION_270]: temp_cursor['x'] += 1 temp_cursor['y'] -= 1 # == # c # = if self.current_tetrimino == TETRIMINO_J: if temp_rotation == ROTATION_0: temp_cursor['x'] += 1 temp_cursor['y'] -= 1 elif temp_rotation == ROTATION_90: temp_cursor['x'] -= 1 elif temp_rotation == ROTATION_270: temp_cursor['y'] += 1 # == # c # = if self.current_tetrimino == TETRIMINO_L: if temp_rotation == ROTATION_90: temp_cursor['y'] += 1 elif temp_rotation == ROTATION_180: temp_cursor['x'] += 1 temp_cursor['y'] -= 1 elif temp_rotation == ROTATION_270: temp_cursor['x'] -= 1 # = = = = # c elif self.current_tetrimino == TETRIMINO_I: # o=oo # o=oo # o=oo # o=oo if temp_rotation == ROTATION_0: temp_cursor['x'] += 1 temp_cursor['y'] -= 1 # oooo # ==== # oooo # oooo elif temp_rotation == ROTATION_90: temp_cursor['x'] -= 1 temp_cursor['y'] += 2 # oo=o # oo=o # oo=o # oo=o elif temp_rotation == ROTATION_180: temp_cursor['x'] += 2 temp_cursor['y'] -= 2 # oooo # oooo # ==== # oooo elif temp_rotation == ROTATION_270: temp_cursor['x'] -= 2 temp_cursor['y'] += 1 # Out of bounds on the left. if temp_cursor['x'] < 0: return False # Out of bounds on the bottom. if temp_cursor['y'] < 0: return False # Check if there is enough space to rotate here. minmax = get_tetrimino_minmax(self.current_tetrimino, temp_rotation) # Out of bounds on the right. if temp_cursor['x'] + minmax['x'] - 1 >= FIELD_WIDTH: return False # Out of bounds on the top. if temp_cursor['y'] + minmax['y'] - 1 >= FIELD_HEIGHT: return False self.temp_gamefield.reset() self.temp_gamefield.put_tetrimino(self.current_tetrimino, temp_rotation, temp_cursor) # There's something in the way. if self.temp_gamefield.collides_with(self.gamefield): return False # Rotation successful. self.current_rotation = temp_rotation self.cursor['x'] = temp_cursor['x'] self.cursor['y'] = temp_cursor['y'] self.dirty = True return True def move_current_tetrimino_down(self): temp_cursor = {} temp_cursor['x'] = self.cursor['x'] temp_cursor['y'] = self.cursor['y'] - 1 self.temp_gamefield.reset() # We're on the ground already. Stay here now. if (temp_cursor['y'] < 0): self.save_current_tetrimino() # Is there space for the new terimino? self.temp_gamefield.reset() self.temp_gamefield.put_tetrimino(self.current_tetrimino, self.current_rotation, self.cursor) if self.temp_gamefield.collides_with(self.gamefield): self.gameover = True # Nope. self.dirty = True return False else: # Is there enough space below? self.temp_gamefield.put_tetrimino(self.current_tetrimino, self.current_rotation, temp_cursor) if self.temp_gamefield.collides_with(self.gamefield): # No. Save it here. self.save_current_tetrimino() # Is there space for the new terimino? self.temp_gamefield.reset() self.temp_gamefield.put_tetrimino(self.current_tetrimino, self.current_rotation, self.cursor) if self.temp_gamefield.collides_with(self.gamefield): self.gameover = True # Nope. self.dirty = True return False else: # Yes, there is. Move the object down for real. self.cursor['y'] -= 1 self.dirty = True return True def save_current_tetrimino(self): # Just assume there is space. self.gamefield.put_tetrimino(self.current_tetrimino, self.current_rotation, self.cursor) self.dirty = True lines_cleared = self.remove_full_lines() # TODO: Display score # Handle combos if self.combo > 0 and lines_cleared == 0: self.score += self.combo * 50 * self.level self.combo = 0 elif lines_cleared > 0: self.combo += 1 # Handle scoring if lines_cleared == 1: self.score += 100 * self.level self.lines_cleared += 1 elif lines_cleared == 2: self.score += 300 * self.level self.lines_cleared += 3 elif lines_cleared == 3: self.score += 500 * self.level self.lines_cleared += 5 elif lines_cleared == 4: self.score += 800 * self.level self.lines_cleared += 8 # Speed up the game each level. while (self.level * 5) <= self.lines_cleared: self.level += 1 if self.level < MAX_LEVEL: self.down_interval -= LEVEL_SPEEDUP_STEP # Show the current level on the leds. self.update_leds() # Give some feedback, that tetrimino is down. vibra.vibrate(lines_cleared * 15 + 10) self.select_random_tetrimino() def update_leds(self): led_colors = [color.CHAOSBLUE] * (MAX_LEVEL - self.level) + [color.RED] * self.level leds.set_all(led_colors) def remove_full_lines(self): removed_lines = 0 # Check every row. y = 0 while y < FIELD_HEIGHT: for x in range(FIELD_WIDTH): # Skip the line if there is a blank square in it. if self.gamefield.field(x, y) == TETRIMINO_NONE: break # We're not at the end of line yet. if x != (FIELD_WIDTH - 1): continue removed_lines += 1 self.gamefield.remove_row(y) # Check this row again, since we moved it all one down. y -= 1 y += 1 return removed_lines def think(self, disp): self.tick += 1 # Any controls pressed? current_presses = buttons.read(buttons.TOP_RIGHT | buttons.BOTTOM_RIGHT | buttons.BOTTOM_LEFT) if current_presses > 0: # Ignore if both buttons were pressed if (current_presses & (buttons.TOP_RIGHT | buttons.BOTTOM_RIGHT)) != (buttons.TOP_RIGHT | buttons.BOTTOM_RIGHT): if (current_presses & buttons.BOTTOM_RIGHT) == buttons.BOTTOM_RIGHT: # Move right self.desired_direction = 1 elif (current_presses & buttons.TOP_RIGHT) == buttons.TOP_RIGHT: # Move left self.desired_direction = -1 # Player wants to rotate? if (current_presses & buttons.BOTTOM_LEFT) == buttons.BOTTOM_LEFT: # Require them to let go of the button again before rotating again. if not self.rotation_button_pressed: self.desire_rotation = True self.rotation_button_pressed = True # Player let go of the rotate button. if (current_presses & buttons.BOTTOM_LEFT) == 0: self.rotation_button_pressed = False # Actuate the controls every x ticks. if (self.tick % TETRIMINO_MOVE_HORIZONTAL_INTERVAL) == 0: if self.desired_direction != 0: self.move_current_tetrimino_leftright() if self.desire_rotation: self.rotate_tetrimino() self.desire_rotation = False # The tetrimino falls slower than it can move sideways or rotate. if (self.tick % self.down_interval) == 0: self.move_current_tetrimino_down() # Check if we even have to redraw the gamefield. if not self.dirty: return # Clear the screen first if something changed. disp.clear() # Render all the changes. self.draw_gamefield(disp) if self.gameover: easydraw.msg("Game Over", fg=(255, 255, 255), bg=(255, 0, 0), posx=14, posy=40) vibra.vibrate(500) # print('cursor: {} {}'.format(self.cursor['y'], self.cursor['x'])) # disp.circ(self.cursor['y']*TETRIMINO_SIZE, self.cursor['x']*TETRIMINO_SIZE, 4, col=(255,255,255), filled=True) disp.update() self.dirty = False def draw_gamefield(self, disp): # print('Drawing game field') self.temp_gamefield.reset() self.temp_gamefield.put_tetrimino(self.current_tetrimino, self.current_rotation, self.cursor) for y in range(FIELD_HEIGHT): draw_y = y * TETRIMINO_SIZE for x in range(FIELD_WIDTH): draw_x = x * TETRIMINO_SIZE tetrimino_type = self.gamefield.field(x, y) if tetrimino_type != TETRIMINO_NONE: # print('Drawing ({} {}) to ({} {})'.format(draw_y, draw_x, draw_y + TETRIMINO_SIZE, draw_x + TETRIMINO_SIZE)) disp.rect(draw_y, draw_x, draw_y + TETRIMINO_SIZE, draw_x + TETRIMINO_SIZE, col=TETRIMINO_COLORS[tetrimino_type], filled=True) else: tetrimino_type = self.temp_gamefield.field(x, y) if tetrimino_type != TETRIMINO_NONE: # print('Drawing current ({} {}) to ({} {})'.format(draw_y, draw_x, draw_y + TETRIMINO_SIZE, draw_x + TETRIMINO_SIZE)) disp.rect(draw_y, draw_x, draw_y + TETRIMINO_SIZE, draw_x + TETRIMINO_SIZE, col=TETRIMINO_COLORS[tetrimino_type], filled=True) tetris = Tetris() tetris.loop()