Move constants out of classes

This commit is contained in:
Nguyễn Gia Phong 2020-09-06 14:24:37 +07:00
parent a024f7a1f9
commit feafd6ca4c
3 changed files with 82 additions and 76 deletions

View File

@ -18,8 +18,13 @@ classifiers = [
'License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)', 'License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)',
'Natural Language :: English', 'Natural Language :: English',
'Operating System :: OS Independent', 'Operating System :: OS Independent',
'Programming Language :: Python :: 3.7',
'Programming Language :: Python :: 3.8',
'Programming Language :: Python :: 3.9',
'Programming Language :: Python :: 3 :: Only',
'Programming Language :: Python', 'Programming Language :: Python',
'Topic :: Games/Entertainment :: Arcade'] 'Topic :: Games/Entertainment :: Arcade']
requires-python = '>=3.7'
keywords = 'stacker,arcade-game,pygame' keywords = 'stacker,arcade-game,pygame'
license = 'GPLv3+' license = 'GPLv3+'

View File

@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
# A clone of the arcade game Stacker # A clone of the arcade game Stacker
# Copyright (C) 2007 Clint Herron # Copyright (C) 2007 Clint Herron
# Copyright (C) 2017-2020 Nguyễn Gia Phong # Copyright (C) 2017-2020 Nguyễn Gia Phong
@ -18,14 +17,16 @@
"""A clone of the arcade game Stacker""" """A clone of the arcade game Stacker"""
__version__ = '2.0.2' __version__ = '2.1.0'
__all__ = ['Slacker'] __all__ = ['Slacker']
from contextlib import ExitStack, redirect_stdout
from importlib.resources import path
from io import StringIO
from math import cos, pi from math import cos, pi
from random import randrange from random import randrange
import pygame with redirect_stdout(StringIO()): import pygame
from pkg_resources import resource_filename
TANGO = {'Butter': ((252, 233, 79), (237, 212, 0), (196, 160, 0)), TANGO = {'Butter': ((252, 233, 79), (237, 212, 0), (196, 160, 0)),
'Orange': ((252, 175, 62), (245, 121, 0), (206, 92, 0)), 'Orange': ((252, 175, 62), (245, 121, 0), (206, 92, 0)),
@ -37,10 +38,21 @@ TANGO = {'Butter': ((252, 233, 79), (237, 212, 0), (196, 160, 0)),
'Aluminium': ((238, 238, 236), (211, 215, 207), (186, 189, 182), 'Aluminium': ((238, 238, 236), (211, 215, 207), (186, 189, 182),
(136, 138, 133), (85, 87, 83), (46, 52, 54))} (136, 138, 133), (85, 87, 83), (46, 52, 54))}
BG_COLOR = TANGO['Aluminium'][5]
COLOR_MAJOR = TANGO['Scarlet Red']
COLOR_MINOR = TANGO['Sky Blue']
MAJOR = 5
def data(resource): INTRO, PLAYING, LOSE, WIN = range(4)
"""Return a true filesystem path for specified resource.""" MAX_WIDTH = (1,)*7 + (2,)*5 + (3,)*3
return resource_filename('slacker_game', resource) WIN_LEVEL = 15
SCREEN_SIZE = SCREEN_WIDTH, SCREEN_HEIGHT = 280, 600
BOARD_SIZE = BOARD_WIDTH, BOARD_HEIGHT = 7, 15
TILE_SIZE = 40
MAX_SPEED, WIN_SPEED, SPEED_DIFF = 70, 100, 5
INIT_SPEED = MAX_SPEED + (BOARD_HEIGHT + 1)*SPEED_DIFF
class SlackerTile: class SlackerTile:
@ -48,20 +60,17 @@ class SlackerTile:
Slacker object for storing tiles. Slacker object for storing tiles.
""" """
SIZE = 40
BG = TANGO['Aluminium'][5]
MAJOR = 5
def __init__(self, screen, x, y, state=1, missed_time=None): def __init__(self, screen, x, y, state=1, missed_time=None):
self.screen, self.x, self.y = screen, x, y self.screen, self.x, self.y = screen, x, y
if state == Slacker.LOSE: if state == LOSE:
self.dim = 1 self.dim = 1
elif missed_time is None: elif missed_time is None:
self.dim = 0 self.dim = 0
else: else:
self.dim = 2 self.dim = 2
self.missed_time = missed_time self.missed_time = missed_time
self.wiggle = state in (Slacker.INTRO, Slacker.WIN) self.wiggle = state in (INTRO, WIN)
def get_xoffset(self, maxoffset, duration=820): def get_xoffset(self, maxoffset, duration=820):
"""Return the offset on x-axis to make the tile complete an cycle of """Return the offset on x-axis to make the tile complete an cycle of
@ -69,7 +78,7 @@ class SlackerTile:
""" """
if self.wiggle: if self.wiggle:
return maxoffset * cos((pygame.time.get_ticks()/float(duration) return maxoffset * cos((pygame.time.get_ticks()/float(duration)
+ self.y/float(Slacker.BOARD_HEIGHT)) * pi) + self.y/float(BOARD_HEIGHT)) * pi)
return 0 return 0
def get_yoffset(self): def get_yoffset(self):
@ -80,72 +89,68 @@ class SlackerTile:
def isfallen(self): def isfallen(self):
"""Return if the tile has fallen off the screen.""" """Return if the tile has fallen off the screen."""
return self.y + self.get_yoffset() > Slacker.BOARD_HEIGHT return self.y + self.get_yoffset() > BOARD_HEIGHT
def draw(self, max_x_offset=2): def draw(self, max_x_offset=2):
"""Draw the tile.""" """Draw the tile."""
if self.y < self.MAJOR: if self.y < MAJOR:
color = Slacker.COLOR_MAJOR color = COLOR_MAJOR
else: else:
color = Slacker.COLOR_MINOR color = COLOR_MINOR
rect = pygame.Rect((self.x+self.get_xoffset(max_x_offset)) * self.SIZE, rect = pygame.Rect((self.x+self.get_xoffset(max_x_offset)) * TILE_SIZE,
(self.y+self.get_yoffset()) * self.SIZE, (self.y+self.get_yoffset()) * TILE_SIZE,
self.SIZE, self.SIZE) TILE_SIZE, TILE_SIZE)
pygame.draw.rect(self.screen, color[self.dim], rect) pygame.draw.rect(self.screen, color[self.dim], rect)
pygame.draw.rect(self.screen, self.BG, rect, self.SIZE // 11) pygame.draw.rect(self.screen, BG_COLOR, rect, TILE_SIZE // 11)
class Slacker: class Slacker:
"""This class provides functions to run the game Slacker, a clone of """This class provides functions to run the game Slacker, a clone of
the popular arcade game Stacker. the popular arcade game Stacker.
""" """
BOARD_SIZE = BOARD_WIDTH, BOARD_HEIGHT = 7, 15
SCREEN_SIZE = SCREEN_WIDTH, SCREEN_HEIGHT = 280, 600
TILE_SIZE = 40
COLOR_MAJOR = TANGO['Scarlet Red']
COLOR_MINOR = TANGO['Sky Blue']
BG_COLOR = TANGO['Aluminium'][5]
ICON = pygame.image.load(data('icon.png'))
MAX_WIDTH = (1,)*7 + (2,)*5 + (3,)*3
MAX_SPEED = 70
SPEED_DIFF = 5
INIT_SPEED = MAX_SPEED + (BOARD_HEIGHT + 1)*SPEED_DIFF
COLOR_CHANGE_Y = 5
WIN_LEVEL = 15
WIN_SPEED = 100
INTRO, PLAYING, LOSE, WIN = range(4)
def __init__(self, restart=False): def __init__(self, restart=False):
pygame.init() self.exit_stack = ExitStack()
pygame.display.set_icon(self.ICON) self.font = self.data('VT323-Regular.ttf')
pygame.display.set_caption('Slacker') self.board = [[False]*BOARD_WIDTH
self.board = [[False]*self.BOARD_WIDTH for _ in range(BOARD_HEIGHT)]
for _ in range(self.BOARD_HEIGHT)] self.game_state = PLAYING if restart else INTRO
self.game_state = self.PLAYING if restart else self.INTRO
self.falling_tiles = [] self.falling_tiles = []
self.screen = pygame.display.set_mode(self.SCREEN_SIZE) self.speed = INIT_SPEED + randrange(5)
self.speed = self.INIT_SPEED + randrange(5)
self.speed_ratio = 1.0 self.speed_ratio = 1.0
self.width = self.MAX_WIDTH[-1] self.width = MAX_WIDTH[-1]
self.y = self.BOARD_HEIGHT - 1 self.y = BOARD_HEIGHT - 1
def __enter__(self):
pygame.init()
pygame.display.set_icon(pygame.image.load(self.data('icon.png')))
pygame.display.set_caption('Slacker')
self.screen = pygame.display.set_mode(SCREEN_SIZE)
return self
def __exit__(self, *exc):
pygame.quit()
self.exit_stack.close()
def data(self, resource):
"""Return a true filesystem path for specified resource."""
return str(self.exit_stack.enter_context(
path('slacker_game', resource)))
def draw_text(self, string, height): def draw_text(self, string, height):
"""Width-fit the string in the screen on the given height.""" """Width-fit the string in the screen on the given height."""
font = pygame.font.Font( font = pygame.font.Font(
data('VT323-Regular.ttf'), self.font, int(SCREEN_WIDTH*2.5/(len(string)+1)))
int(self.SCREEN_WIDTH * 2.5 / (len(string) + 1))) text = font.render(string, False, COLOR_MINOR[0])
text = font.render(string, False, self.COLOR_MINOR[0]) self.screen.blit(text, ((SCREEN_WIDTH - text.get_width()) // 2,
self.screen.blit(text, ((self.SCREEN_WIDTH - text.get_width()) // 2, int(SCREEN_HEIGHT * height)))
int(self.SCREEN_HEIGHT * height)))
def intro(self): def intro(self):
"""Draw the intro screen.""" """Draw the intro screen."""
for i in [(2, 2), (3, 2), (4, 2), (1.5, 3), (4.5, 3), for i in [(2, 2), (3, 2), (4, 2), (1.5, 3), (4.5, 3),
(1.5, 4), (2, 5), (3, 5), (4, 5), (4.5, 6), (1.5, 4), (2, 5), (3, 5), (4, 5), (4.5, 6),
(1.5, 7), (4.5, 7), (2, 8), (3, 8), (4, 8)]: (1.5, 7), (4.5, 7), (2, 8), (3, 8), (4, 8)]:
SlackerTile(self.screen, *i, state=self.INTRO).draw(1.5) SlackerTile(self.screen, *i, state=INTRO).draw(1.5)
if pygame.time.get_ticks() // 820 % 2: if pygame.time.get_ticks() // 820 % 2:
self.draw_text('Press Spacebar', 0.75) self.draw_text('Press Spacebar', 0.75)
@ -165,30 +170,30 @@ class Slacker:
def update_screen(self): def update_screen(self):
"""Draw the whole screen and everything inside.""" """Draw the whole screen and everything inside."""
self.screen.fill(self.BG_COLOR) self.screen.fill(BG_COLOR)
if self.game_state == self.INTRO: if self.game_state == INTRO:
self.intro() self.intro()
elif self.game_state in (self.PLAYING, self.LOSE, self.WIN): elif self.game_state in (PLAYING, LOSE, WIN):
self.draw_board() self.draw_board()
pygame.display.flip() pygame.display.flip()
def update_movement(self): def update_movement(self):
"""Update the direction the blocks are moving in.""" """Update the direction the blocks are moving in."""
speed = self.speed * self.speed_ratio speed = self.speed * self.speed_ratio
positions = self.BOARD_WIDTH + self.width - 2 positions = BOARD_WIDTH + self.width - 2
p = int(round(pygame.time.get_ticks() / speed)) % (positions * 2) p = int(round(pygame.time.get_ticks() / speed)) % (positions * 2)
self.x = (-p % positions if p > positions else p) - self.width + 1 self.x = (-p % positions if p > positions else p) - self.width + 1
self.board[self.y] = [0 <= x - self.x < self.width self.board[self.y] = [0 <= x - self.x < self.width
for x in range(self.BOARD_WIDTH)] for x in range(BOARD_WIDTH)]
def key_hit(self): def key_hit(self):
"""Process the current position of the blocks relatively to the """Process the current position of the blocks relatively to the
ones underneath when user hit the switch, then decide if 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. user will win, lose or go to the next level of the tower.
""" """
if self.y < self.BOARD_HEIGHT - 1: if self.y < BOARD_HEIGHT - 1:
for x in range(max(0, self.x), for x in range(max(0, self.x),
min(self.x + self.width, self.BOARD_WIDTH)): min(self.x + self.width, BOARD_WIDTH)):
# If there isn't any block underneath # If there isn't any block underneath
if not self.board[self.y + 1][x]: if not self.board[self.y + 1][x]:
# Get rid of the block not standing on solid ground # Get rid of the block not standing on solid ground
@ -199,28 +204,28 @@ class Slacker:
missed_time=pygame.time.get_ticks())) missed_time=pygame.time.get_ticks()))
self.width = sum(self.board[self.y]) self.width = sum(self.board[self.y])
if not self.width: if not self.width:
self.game_state = self.LOSE self.game_state = LOSE
elif not self.y: elif not self.y:
self.game_state = self.WIN self.game_state = WIN
else: else:
self.y -= 1 self.y -= 1
self.width = min(self.width, self.MAX_WIDTH[self.y]) self.width = min(self.width, MAX_WIDTH[self.y])
self.speed = self.MAX_SPEED + self.y*self.SPEED_DIFF + randrange(5) self.speed = MAX_SPEED + self.y*SPEED_DIFF + randrange(5)
def main_loop(self, loop=True): def main_loop(self, loop=True):
"""The main loop.""" """The main loop."""
while loop: while loop:
if self.game_state == self.INTRO: if self.game_state == INTRO:
for event in pygame.event.get(): for event in pygame.event.get():
if event.type == pygame.QUIT: if event.type == pygame.QUIT:
loop = False loop = False
elif event.type == pygame.KEYDOWN: elif event.type == pygame.KEYDOWN:
if event.key == pygame.K_SPACE: if event.key == pygame.K_SPACE:
self.game_state = self.PLAYING self.game_state = PLAYING
elif event.key in (pygame.K_ESCAPE, pygame.K_q): elif event.key in (pygame.K_ESCAPE, pygame.K_q):
loop = False loop = False
elif self.game_state == self.PLAYING: elif self.game_state == PLAYING:
for event in pygame.event.get(): for event in pygame.event.get():
if event.type == pygame.QUIT: if event.type == pygame.QUIT:
loop = False loop = False
@ -231,12 +236,12 @@ class Slacker:
self.__init__() self.__init__()
# Yes, this is a cheat. # Yes, this is a cheat.
elif event.key == pygame.K_0: elif event.key == pygame.K_0:
self.width += self.width < self.BOARD_WIDTH self.width += self.width < BOARD_WIDTH
elif event.key in range(pygame.K_1, pygame.K_9+1): elif event.key in range(pygame.K_1, pygame.K_9+1):
self.speed_ratio = (pygame.K_9-event.key+1) / 5.0 self.speed_ratio = (pygame.K_9-event.key+1) / 5.0
self.update_movement() self.update_movement()
elif self.game_state in (self.LOSE, self.WIN): elif self.game_state in (LOSE, WIN):
for event in pygame.event.get(): for event in pygame.event.get():
if event.type == pygame.QUIT: if event.type == pygame.QUIT:
loop = False loop = False

View File

@ -1,13 +1,9 @@
import pygame
from slacker_game import Slacker from slacker_game import Slacker
def main(): def main():
pygame.init() with Slacker() as slacker:
slacker = Slacker() slacker.main_loop()
slacker.main_loop()
pygame.display.quit()
if __name__ == '__main__': main() if __name__ == '__main__': main()