slacker-game/slacker-game

229 lines
8.7 KiB
Python
Executable File

#!/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 <http://www.gnu.org/licenses/>.
#
# 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()