#!/usr/bin/env python # -*- coding: utf-8 -*- # slacker-game - A clone of the arcade game Stacker # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . # # Copyright (C) 2007 Clint Herron # Copyright (C) 2017 Nguyễn Gia Phong from math import sin from random import choice, randint import pygame from pygame.locals import * class SlackerMissedTile: """SlackerMissedTile(x, y, time) => SlackerMissedTile Slacker object for storing missed tiles. """ def __init__(self, x, y, time): self.x, self.y, self.time = x, y, time def get_time_delta(self): """Return the duration the missed tile has been falling in.""" return (pygame.time.get_ticks() - self.time) / 125.0 class Slacker: """This class provides functions to run the game Slacker, a clone of the popular arcade game Stacker. """ BOARD_SIZE = BOARD_WIDTH, BOARD_HEIGHT = 7, 15 SCREEN_SIZE = SCREEN_WIDTH, SCREEN_HEIGHT = 280, 600 TILE_SIZE = TILE_WIDTH, TILE_HEIGHT = 40, 40 TILE_COLOR = 127, 127, 255 TILE_COLOR_ALT = 255, 127, 127 TILE_COLOR_LOSE = 64, 64, 128 TILE_COLOR_ALT_LOSE = 127, 64, 64 BLACK = 0, 0, 0 LEVEL_SPEED = 80, 80, 75, 75, 70, 70, 65, 60, 55, 50, 45, 40, 35, 30, 32 MAX_WIDTH = (3,)*4 + (2,)*4 + (1,)*7 COLOR_CHANGE_Y = 5 # blocks below which are displayed in the alternate color WIN_LEVEL = 15 WIN_SPEED = 100 INTRO, PLAYING, LOSE, WIN = range(4) BG_IMAGES = [pygame.image.load('data/_{}.png'.format(i)) for i in ('intro', 'game', 'lose', 'win')] BG_IMAGES[WIN].set_colorkey(BLACK) BG_IMAGES[LOSE].set_colorkey(BLACK) def __init__(self): self.board = [[False] * self.BOARD_WIDTH for _ in range(self.BOARD_HEIGHT)] self.direction = choice([1, -1]) self.game_state = self.INTRO self.level = 0 self.last_time = 0 self.missed_tiles = [] self.screen = pygame.display.set_mode(self.SCREEN_SIZE) self.speed = self.LEVEL_SPEED[0] self.speed_ratio = 1.0 self.width = self.MAX_WIDTH[0] self.x = randint(0, self.BOARD_WIDTH - self.width) self.y = self.BOARD_HEIGHT - 1 def draw_tile(self, x, y): """Draw the tile at position (x, y).""" if self.game_state == self.LOSE: if y < self.COLOR_CHANGE_Y: color = self.TILE_COLOR_ALT_LOSE else: color = self.TILE_COLOR_LOSE else: if y < self.COLOR_CHANGE_Y: color = self.TILE_COLOR_ALT else: color = self.TILE_COLOR # XOffset is used to draw some wiggle in the tower when you win if self.game_state == self.WIN: xoffset = (sin(pygame.time.get_ticks()*0.004 + y*0.5) * (self.SCREEN_WIDTH / 4)) else: xoffset = 0 rect = pygame.Rect(x*self.TILE_WIDTH + xoffset, y * self.TILE_HEIGHT, self.TILE_WIDTH, self.TILE_HEIGHT) pygame.draw.rect(self.screen, color, rect) pygame.draw.rect(self.screen, self.BLACK, rect, 2) def draw_board(self): """Draw the board and the tiles inside.""" for x in range(self.BOARD_WIDTH): for y in range(self.BOARD_HEIGHT): if self.board[y][x]: self.draw_tile(x, y) # Draw the missed tiles for mt in self.missed_tiles: time_delta = mt.get_time_delta() x = mt.x * self.TILE_WIDTH y = mt.y*self.TILE_HEIGHT + time_delta**2 if mt.y < self.COLOR_CHANGE_Y: color = self.TILE_COLOR_ALT_LOSE else: color = self.TILE_COLOR_LOSE if y > self.SCREEN_HEIGHT: self.missed_tiles.remove(mt) else: pygame.draw.rect( self.screen, color, pygame.Rect(x + 2, y + 2, self.TILE_WIDTH - 3, self.TILE_HEIGHT - 3)) def draw_background(self): """Draw the background image according to current game_state.""" self.screen.blit(self.BG_IMAGES[self.game_state], (0, 0, self.SCREEN_WIDTH, self.SCREEN_HEIGHT)) def update_screen(self): """Draw the whole screen and everything inside.""" if self.game_state == self.PLAYING: self.draw_background() self.draw_board() elif self.game_state == self.INTRO: self.draw_background() elif self.game_state in (self.LOSE, self.WIN): self.screen.fill(self.BLACK) self.draw_board() self.draw_background() pygame.display.flip() def update_movement(self): """Update the direction the blocks are moving in.""" self.time = pygame.time.get_ticks() if self.last_time + self.speed * self.speed_ratio <= self.time: if not -self.width < self.x + self.direction < self.BOARD_WIDTH: self.direction *= -1 self.x += self.direction self.last_time = self.time self.board[self.y] = [0 <= x - self.x < self.width for x in range(self.BOARD_WIDTH)] def key_hit(self): """Process the current position of the blocks relatively to the ones underneath when user hit the switch, then decide if the user will win, lose or go to the next level of the tower. """ if self.y < self.BOARD_HEIGHT - 1: for x in range(max(0, self.x), min(self.x + self.width, self.BOARD_WIDTH)): # If there isn't any block underneath if not self.board[self.y + 1][x]: # Get rid of the block not standing on solid ground self.board[self.y][x] = False # Then, add that missed block to missed_tiles self.missed_tiles.append( SlackerMissedTile(x, self.y, pygame.time.get_ticks())) self.width = sum(self.board[self.y]) if not self.width: self.game_state = self.LOSE elif self.level + 1 == self.WIN_LEVEL: self.speed, self.game_state = self.WIN_SPEED, self.WIN else: self.direction = choice([1, -1]) self.level += 1 self.speed = self.LEVEL_SPEED[self.level] self.width = min(self.width, self.MAX_WIDTH[self.level]) self.x = randint(0, self.BOARD_WIDTH - self.width) self.y -= 1 def main_loop(self, loop=True): """The main loop.""" while loop: self.update_screen() if self.game_state == self.INTRO: for event in pygame.event.get(): if event.type == pygame.QUIT: loop = False elif event.type == KEYDOWN: if event.key in (K_RETURN, K_SPACE): self.game_state = self.PLAYING elif event.key in (K_ESCAPE, K_q): loop = False elif self.game_state == self.PLAYING: for event in pygame.event.get(): if event.type == pygame.QUIT: loop = False elif event.type == KEYDOWN: if event.key in (K_RETURN, K_SPACE): self.key_hit() elif event.key in (K_ESCAPE, K_q): self.__init__() # Yes, this is a cheat. elif event.key == K_F1 and self.width < self.BOARD_WIDTH: self.x -= self.direction self.width += 1 elif event.key in range(K_1, K_9 + 1): self.speed_ratio = (K_9 - event.key + 1) / 5.0 self.update_movement() elif self.game_state in (self.LOSE, self.WIN): for event in pygame.event.get(): if event.type == pygame.QUIT: loop = False elif event.type == KEYDOWN: self.__init__() if __name__ == '__main__': pygame.init() slacker = Slacker() slacker.main_loop() pygame.display.quit()