Initial Commit

This commit is contained in:
davidovski 2023-01-05 11:35:28 +00:00
commit b99dbb396c
19 changed files with 3912 additions and 0 deletions

203
boss.py Normal file
View file

@ -0,0 +1,203 @@
from dataclasses import replace
import math
from enemy import EnemyAttributes
from formation import (
CircleFormation,
CircleFormationAttributes,
EnemyFormation,
FormationAttributes,
FormationEnemy,
)
from game import Game
class CircleBossFormation(EnemyFormation):
"""Enemy Formation for the circular game boss"""
RADIUS = 10
CYCLE_PEROID = 500
COUNT = 6
def __init__(self, game: Game, attributes: EnemyAttributes):
"""Initialise the circle boss
:param game: The game which this boss belongs to
:type game: Game
:param attributes: The attributes on which to base spawned enemies
:type attributes: EnemyAttributes
"""
self.image_name = "enemy0"
self.minion_image_name = "smallenemy0"
self.alpha = 0
attributes = CircleFormationAttributes(
radius=40,
period=300,
count=CircleBossFormation.COUNT,
velocity=attributes.velocity,
cooldown=attributes.cooldown,
hp=attributes.hp//2,
reward=attributes.reward*10
)
self.circle_formation = CircleFormation(
game, self.minion_image_name, attributes)
super().__init__(game, self.image_name, attributes)
def create_enemies(self):
"""Spawn the boss"""
self.spawn_enemy((-4, -4, 0))
def tick(self, player):
"""Update the boss's position
:param player: The player which to check collision with
:type player: Player
"""
super().tick(player)
self.circle_formation.x = self.x
self.circle_formation.y = self.y
a = (self.alpha/CircleBossFormation.CYCLE_PEROID)*2*math.pi - math.pi
r = 50*math.sin(a) - 25
p = math.sin(a*2)*100
self.circle_formation.attributes.radius = math.floor(
CircleBossFormation.RADIUS + (r if r > 0 else 0)
)
self.circle_formation.attributes.period = math.floor(
400 + (p if p < 100 else 100)
)
self.circle_formation.tick(player)
# When the boss is dead, the minions will all die
if len(self.sprites) == 0:
if len(self.circle_formation.sprites) > 0:
self.circle_formation.sprites[0].damage()
else:
self.destroy()
def destroy(self):
"""Remove the circle boss"""
super().destroy()
self.circle_formation.destroy()
def hide(self):
"""Hide the circle boss"""
self.circle_formation.hide()
return super().hide()
def show(self):
"""Show the circle boss"""
self.circle_formation.show()
return super().show()
class SnakeBossFormation(EnemyFormation):
"""Enemy formation for the snake boss"""
LENGTH = 32
def __init__(self, game: Game, attributes: FormationAttributes):
"""Initialise the snake boss
:param game: The game which the boss belongs to
:type game: Game
:param attributes: The attributes of which to base spawned enemies on
:type attributes: FormationAttributes
"""
self.minion_name = "smallenemy1"
self.tail_name = "smallenemy1_evil"
self.head_name = "enemy2"
self.phase = 1
self.phase_timer = 0
super().__init__(game, self.minion_name, attributes)
def create_enemies(self):
"""Spawn the snake"""
head_attributes = replace(self.attributes)
head_attributes.hp *= 100
self.head = FormationEnemy(self.game, self.head_name,
(0, 0, 0), head_attributes)
self.sprites.append(self.head)
for i in range(SnakeBossFormation.LENGTH):
self.spawn_enemy((0, 0, i+1))
tail_attributes = replace(self.attributes)
head_attributes.hp //= 5
self.tail = FormationEnemy(self.game, self.tail_name,
(0, 0, SnakeBossFormation.LENGTH+1),
tail_attributes)
self.sprites.append(self.tail)
def spawn_enemy(self, offset):
"""Spawn one enemy unit of the snake
:param offset: The offset of the enemy
"""
attributes = replace(self.attributes)
if offset[2] % 6 == 0:
attributes.cooldown = 40
else:
attributes.cooldown = -1
enemy = FormationEnemy(self.game, self.image_name, offset, attributes)
self.sprites.append(enemy)
return enemy
def position_enemy(self, enemy: FormationEnemy):
"""Position the enemy on the game screen
:param enemy: The enemy to position
:type enemy: FormationEnemy
"""
if self.phase == 2:
p = 120 / (100 + math.cos(self.phase_timer / 400)*20) * 120
else:
p = 120
m = 4
t = ((-enemy.offset_a*m) + self.game.alpha) / p + math.pi
a = self.game.w // 2
b = self.game.h // 3
c = 0
if self.phase == 2:
n = 10 - (2000 / (self.phase_timer+2000))*5
else:
n = 5
enemy.set_pos((
int(self.x + a*math.sin(n*t+c)),
int(self.y + b*math.sin(t))
))
def tick(self, player):
"""Update the position of the enemies
:param player: The player which to check collision with
"""
super().tick(player)
if self.phase == 1:
self.head.hp = self.attributes.hp*100
if self.tail.destroyed:
if len(self.sprites) > 1:
self.sprites[-1].damage(amount=(self.attributes.hp//4))
else:
self.head.hp = self.attributes.hp * 3
self.phase = 2
elif self.phase == 2:
self.phase_timer += 1
self.head.attributes.cooldown = int(
20 + math.sin(self.phase_timer / 50)*10)

127
boss_key.py Normal file
View file

@ -0,0 +1,127 @@
from config import Config
from game import Game
class BossKey():
"""Object which manages the 'boss key' feature
When a key is pressed, then the screen switches to a "work"
related image
"""
FG = "#ffaa00"
BG = "#aaaaaa"
BG2 = "#ffffff"
FG2 = "#555555"
TEXT_SIZE = 30
def __init__(self, game: Game, pause_callback) -> None:
"""Initialises the boss key feature
:param game: The game which to use
:type game: Game
:param pause_callback: The function to call to pause the game
:rtype: None
"""
self.game = game
self.canvas = game.canvas
self.width, self.height = game.w * Config.SCALE, game.h * Config.SCALE
self.shapes = []
self.game.inputs.add_keypress_handler(self.on_key)
self.hidden = True
self.pause_callback = pause_callback
def on_key(self, event):
"""Handle key press events
:param event: The key press event
"""
if event.keysym == self.game.inputs.settings.boss \
and self.hidden:
self.pause_callback()
self.create_shapes()
self.hidden = False
return True
if not self.hidden:
self.delete_shapes()
self.hidden = True
return True
return False
def create_rectangle(self, x, y, w, h, color):
"""Create a rectangle object
:param x: x coordinate
:param y: y coordinate
:param w: width
:param h: height
:param color: The colour of the rectangle
"""
self.shapes.append(self.canvas.create_rectangle(
x, y, x+w, y+h, fill=color, state="disabled"))
def write_text(self, x, y, text):
"""Create a text object
:param x: x coordiante
:param y: y coordinate
:param text: The text used for this label
"""
self.shapes.append(self.canvas.create_text(
x, y, text=text, fill=BossKey.BG2,
font=(f"Helvetica {BossKey.TEXT_SIZE} bold"), state="disabled"))
def create_shapes(self):
"""Create all the shapes needed for the calculator"""
width = self.width
height = self.height
padding = width // 50
num_rows = 5
num_cols = 4
grid_width = width // num_cols
grid_height = height // (num_rows+1)
self.create_rectangle(0, 0, width, height, BossKey.BG)
self.create_rectangle(padding,
padding,
width - padding*2,
grid_height-padding*2,
BossKey.FG2)
symbols = [
"(", ")", "%", "AC",
"7", "8", "9", "/",
"4", "5", "6", "x",
"1", "2", "3", "-",
"0", ".", "=", "+"
]
for row in range(num_rows):
for col in range(num_cols):
color = BossKey.FG2
if row == 0 or col == num_cols - 1:
color = BossKey.FG
x = col*grid_width+padding
y = row*grid_height+padding+grid_height
w = grid_width-padding*2
h = grid_height-padding*2
self.create_rectangle(x, y, w, h, color)
offset_x = x + padding + (
grid_width
- padding*2
- BossKey.TEXT_SIZE) // 2
offset_y = y + padding + (
grid_height-padding * 2
- BossKey.TEXT_SIZE) // 2
symbol = symbols[col + row*num_cols]
self.write_text(offset_x, offset_y, symbol)
def delete_shapes(self):
"""Remove all the shapes used for the calculator"""
for shape in self.shapes:
self.canvas.delete(shape)

162
cheat_engine.py Normal file
View file

@ -0,0 +1,162 @@
from typing import Callable, List
from config import Config
from menu import Menu
class Cheat:
"""A single cheat, to be assigned to a cheat engine"""
def __init__(self, game, code: List[str], callback: Callable):
"""Initialise the cheat
:param game: The game which the cheat belongs to
:param code: A string of key codes to be pressed to activate the cheat
:type code: List[str]
:param callback: The function to be called when this cheat is activated
:type callback: Callable
"""
self.game = game
self.code = code
self.callback = callback
self.position = 0
def on_key(self, event):
"""Handle a key press event
:param event: The event that is being handled
"""
if self.position < len(self.code):
next_key = self.code[self.position]
if event.keysym == next_key:
self.position += 1
if self.position == len(self.code):
self.callback()
self.position = 0
else:
self.position = 0
return False
class InvincibilityCheat(Cheat):
"""Cheat that makes the player invincible"""
def __init__(self, game, code: List[str]):
"""Initialise the cheat
:param game: The game which this belongs to
:param code: The combination of characters for which to
activate this cheat
:type code: List[str]
"""
super().__init__(game, code, self.toggle)
self.enabled = False
self.damage_function = None
def toggle(self):
"""Enable or disable this cheat"""
self.enabled = not self.enabled
if self.enabled:
self.game.effect_player.splash_text("Godmode on")
self.game.player.set_image(self.game.player.white_image)
self.damage_function = self.game.player.damage
self.game.player.damage = (lambda: None)
else:
self.game.effect_player.splash_text("Godmode off")
self.game.player.set_image(
self.game.texture_factory.get_image("ship"))
self.game.player.damage = self.damage_function
class DevModeCheat(Cheat):
"""Cheat that enables 'dev mode' which:
- enables spawning menu
- key to remove all enemies
- key to stop spawning outright
"""
def __init__(self, game, code: List[str]):
"""Initialise the cheat
:param game: The game which this belongs to
:param code: The combination of characters which activate this cheat
:type code: List[str]
"""
super().__init__(game, code, self.toggle)
self.enabled = Config.DEVMODE
self.spawning_disabled = False
self.spawn_menu = Menu(self.game, "Spawn Menu")
for i in ("circle_boss",
"snake_boss",
"loop",
"orbital",
"rectangle",
"fleet"):
self.spawn_menu.add_item(i, self.spawn_item(i))
def toggle(self):
"""Toggle if this mode is enabled"""
self.enabled = not self.enabled
if self.enabled:
self.game.effect_player.splash_text("devmode on")
else:
self.game.effect_player.splash_text("devmode off")
def spawn_item(self, name):
"""Spawn a named item from the menu
:param name: The name of the formation to spawn
"""
return lambda: (
self.spawn_menu.hide(),
getattr(self.game.formation_spawner, f"spawn_{name}")()
)
def on_key(self, event):
"""Handle Key press events
:param event: The key press event to handle
"""
if self.enabled:
if event.keysym == "n":
self.game.formation_spawner.clear_all()
self.game.formation_spawner.next_phase()
if event.keysym == "c":
self.game.formation_spawner.clear_all()
if event.keysym == "k":
if self.spawning_disabled:
self.game.effect_player.splash_text("spawning on")
self.spawning_disabled = False
self.game.formation_spawner.next_formation = 0
else:
self.game.effect_player.splash_text("spawning off")
self.game.formation_spawner.clear_all()
self.game.formation_spawner.next_formation = -1
if event.keysym == "m":
self.spawn_menu.show()
return super().on_key(event)
class CheatEngine:
"""Object which manages cheats"""
def __init__(self, game):
"""Initialise the cheat engine
:param game: The game which this belongs to
"""
self.game = game
self.cheats = []
def add_cheat(self, cheat):
"""Register a cheat to the engine
:param cheat: The cheat to be registered
"""
self.game.inputs.add_keypress_handler(cheat.on_key)
self.cheats.append(cheat)

20
config.py Normal file
View file

@ -0,0 +1,20 @@
from dataclasses import dataclass
@dataclass
class Config:
"""Various constants and configuration for the game"""
WIDTH = 100
HEIGHT = 200
# Number of window pixels used for each game "pixel"
SCALE = 6
FPS = 30
NICK_LEN = 3
DEVMODE = False
LEADERBOARD_FILE = "leaderboard"
SAVE_FILE = "save"
SETTINGS_FILE = "settings"

63
enemy.py Normal file
View file

@ -0,0 +1,63 @@
from dataclasses import dataclass
from game import Game
from shooter import Shooter, ShooterAttributes
@dataclass
class EnemyAttributes(ShooterAttributes):
"""Attributes of an enemy object"""
reward: int = 100
lazer_color: str = "red"
cooldown: int = 20
class Enemy(Shooter):
"""An enemy in the game"""
def __init__(self, game: Game, image_name: str,
attributes: EnemyAttributes):
"""Initialise the enemy
:param game: The game which this belongs to
:type game: Game
:param image_name: The name of the image to use
:type image_name: str
:param attributes: The attributes of this
:type attributes: EnemyAttributes
"""
super().__init__(game, image_name, attributes)
self.attributes = attributes
def tick(self, player):
"""Check for collisions and shoot
:param player: The player which to check collisions with
"""
super().tick()
if self.attributes.cooldown != -1:
self.shoot()
lazer_collisions = self.collide_all(player.lazers)
if lazer_collisions != -1:
self.damage()
player.lazers[lazer_collisions].destroy()
player_collisions = player.collide_all(self.lazers)
if player_collisions != -1:
player.damage()
self.lazers[player_collisions].destroy()
if self.collides(player):
player.damage()
self.damage()
def damage(self, amount=1):
"""Reduce the object's health
:param amount:
"""
super().damage(amount)
if self.destroyed:
self.game.score += self.attributes.reward

342
font.py Normal file
View file

@ -0,0 +1,342 @@
class Font:
"""Convert a pixel font into photoimages"""
FONT_SIZE = 5
# effective width of each character, including letter spacing
FONT_WIDTH = 6
CHARS = {
"\0": [
" ",
" ",
" ",
" ",
" ",
],
"0": [
" xxx ",
"x xx",
"x x x",
"xx x",
" xxx ",
],
"1": [
" x ",
" xx ",
" x ",
" x ",
"xxxxx",
],
"2": [
"xxxx ",
" x",
" xxx ",
"x ",
"xxxxx",
],
"3": [
"xxxx ",
" x",
"xxxx ",
" x",
"xxxx ",
],
"4": [
"x x",
"x x",
"xxxxx",
" x",
" x",
],
"5": [
"xxxxx",
"x ",
" xxx ",
" x",
"xxxx ",
],
"6": [
" xxx ",
"x ",
"xxxx ",
"x x",
" xxx ",
],
"7": [
"xxxxx",
" x",
" x ",
" x ",
" x ",
],
"8": [
" xxx ",
"x x",
" xxx ",
"x x",
" xxx ",
],
"9": [
" xxx ",
"x x",
" xxxx",
" x",
" x",
],
"a": [
" xxx ",
"x x",
"xxxxx",
"x x",
"x x",
],
"b": [
"xxxx ",
"x x",
"xxxx ",
"x x",
"xxxx ",
],
"c": [
" xxxx",
"x ",
"x ",
"x ",
" xxxx",
],
"d": [
"xxxx ",
"x x",
"x x",
"x x",
"xxxx ",
],
"e": [
"xxxxx",
"x ",
"xxxxx",
"x ",
"xxxxx",
],
"f": [
"xxxxx",
"x ",
"xxxxx",
"x ",
"x ",
],
"g": [
" xxxx",
"x ",
"x xx",
"x x",
" xxxx",
],
"h": [
"x x",
"x x",
"xxxxx",
"x x",
"x x",
],
"i": [
"xxxxx",
" x ",
" x ",
" x ",
"xxxxx",
],
"j": [
"xxxxx",
" x",
" x",
" x",
"xxxx ",
],
"k": [
"x x",
"x x ",
"xxx ",
"x x ",
"x x",
],
"l": [
"x ",
"x ",
"x ",
"x ",
"xxxxx",
],
"m": [
"x x",
"xx xx",
"x x x",
"x x",
"x x",
],
"n": [
"x x",
"xx x",
"x x x",
"x xx",
"x x",
],
"o": [
" xxx ",
"x x",
"x x",
"x x",
" xxx ",
],
"p": [
"xxxx ",
"x x",
"xxxx ",
"x ",
"x ",
],
"q": [
" xxx ",
"x x",
"x x",
"x x ",
" xx x",
],
"r": [
"xxxx ",
"x x",
"xxxx ",
"x x",
"x x",
],
"s": [
" xxxx",
"x ",
" xxx ",
" x",
"xxxx ",
],
"t": [
"xxxxx",
" x ",
" x ",
" x ",
" x ",
],
"u": [
"x x",
"x x",
"x x",
"x x",
" xxx ",
],
"v": [
"x x",
"x x",
"x x",
" x x ",
" x ",
],
"w": [
"x x",
"x x",
"x x x",
"x x x",
" x x ",
],
"x": [
"x x",
" x x ",
" x ",
" x x ",
"x x",
],
"y": [
"x x",
" x x ",
" x ",
" x ",
" x ",
],
"z": [
"xxxxx",
" x ",
" x ",
" x ",
"xxxxx",
],
" ": [
" ",
" ",
" ",
" ",
" ",
],
">": [
" x ",
" x ",
" x ",
" x ",
" x ",
],
"<": [
" x ",
" x ",
" x ",
" x ",
" x ",
],
}
@staticmethod
def _create_font_texture(text, color="#fff", letter_space=1):
"""Convert a font array into a game texture
:param text: the characters used within the font
:param color: The colour to use
:param letter_space: The spacing between each letter to use
"""
string = Font._create_characters(text, letter_space)
return [
[
None if character == " " else color
for character in row
] for row in string
]
@staticmethod
def _create_characters(text, letter_space):
"""Concatenate font symbols
:param text: The text of the font
:param letter_space: The spacing between each letter
"""
# create a list of all characters in the string
characters = [
Font.CHARS[c] if c in Font.CHARS else Font.CHARS["\0"]
for c in text.lower()
]
# join each row of each character into one "character"
return [
(" "*letter_space).join([c[row] for c in characters])
for row in range(Font.FONT_SIZE)
]
@staticmethod
def load_text(texture_factory, text, color="#fff", letter_space=1):
"""Create and load text into a photo image
:param texture_factory: The texture factory used for processing
:param text: The text to convert
:param color: Color of the text
:param letter_space: Spacing between letters
"""
return texture_factory.load_texture(f"text:{text}",
Font._create_font_texture(
text,
color=color,
letter_space=letter_space)
)

296
formation.py Normal file
View file

@ -0,0 +1,296 @@
from dataclasses import dataclass
import math
from typing import List
from enemy import Enemy, EnemyAttributes
from game import Game
from sprite import Sprite
@dataclass
class FormationAttributes(EnemyAttributes):
"""FormationAttributes."""
count: int = 1
class FormationEnemy(Enemy):
"""An enemy that belongs to a formation"""
def __init__(self, game: Game, image_name, offset,
attributes: EnemyAttributes):
"""Initialise the enemy
:param game: The game which this belongs to
:type game: Game
:param image_name: The name of the image to use for the enemy
:param offset: The offset from the other enemies
:param attributes: The attributes given to this enemy
:type attributes: EnemyAttributes
"""
self.offset_x, self.offset_y, self.offset_a = offset
super().__init__(game, image_name, attributes)
class EnemyFormation:
"""Cluster of enemies that move in a particular way"""
def __init__(self, game: Game, image_name: str,
enemy_attributes: FormationAttributes):
"""Initialise the formation
:param game: The game which this belongs to
:type game: Game
:param image_name: The name of the image to use for the enemy
:type image_name: str
:param enemy_attributes: The attributes to use for spawned enemies
:type enemy_attributes: FormationAttributes
"""
self.game = game
self.sprites: List[FormationEnemy] = []
self.image_name = image_name
self.alpha = 0
self.attributes = enemy_attributes
self.x, self.y = 0, 0
self.destroyed = False
self.create_enemies()
self.hidden = True
def create_enemies(self):
"""Spawn enemies"""
pass
def position_enemy(self, enemy: FormationEnemy):
"""Position a single enemy
:param enemy: The enemy to position
:type enemy: FormationEnemy
"""
enemy.set_pos(
(
int(self.x + enemy.offset_x),
int(self.y + enemy.offset_y)
)
)
def spawn_enemy(self, offset):
"""Spawn a single enemy
:param offset: The offset which to apply to the enemy
"""
enemy = FormationEnemy(self.game, self.image_name,
offset, self.attributes)
self.sprites.append(enemy)
return enemy
def tick(self, player):
"""Update the positions of all enemies
:param player: The player to check if the enemies collide with
"""
self.alpha += 1
for enemy in self.sprites:
enemy.tick(player)
self.position_enemy(enemy)
self.sprites = Sprite.remove_destroyed(self.sprites)
if len(self.sprites) == 0:
self.destroy()
def destroy(self):
"""Delete all enemies in this formation"""
for enemy in self.sprites:
enemy.destroy()
self.sprites = []
self.destroyed = True
def set_pos(self, pos):
"""Set the position of this formation
:param pos: position to move to
"""
self.x, self.y = pos
def show(self):
"""Make this formation visible"""
if self.hidden:
for enemy in self.sprites:
enemy.show()
self.hidden = False
def hide(self):
"""Make this formation hidden"""
if not self.hidden:
for enemy in self.sprites:
enemy.hide()
self.hidden = True
@dataclass
class CircleFormationAttributes(FormationAttributes):
"""Attributes for a circle formation"""
radius: int = 40
period: int = 300
class CircleFormation(EnemyFormation):
"""A circular formation of enemies, rotating in a ring"""
def __init__(self, game: Game, image_name,
attributes: CircleFormationAttributes):
"""Initialise the formation
:param game: The game which this belongs to
:type game: Game
:param image_name: The name of the image to use for the enemy
:param attributes: The attributes to use for spawned enemies
:type attributes: CircleFormationAttributes
"""
super().__init__(game, image_name, attributes)
self.attributes: CircleFormationAttributes
def create_enemies(self):
"""Spawn all the enemies"""
for i in range(self.attributes.count):
self.spawn_enemy((0, 0, i))
def position_enemy(self, enemy: FormationEnemy):
"""Position a single enemy
:param enemy:
:type enemy: FormationEnemy
"""
a = (enemy.offset_a / self.attributes.count) * \
self.attributes.period + self.game.alpha
enemy.set_pos(
(
int(
self.x+math.sin((-a/self.attributes.period)
* 2*math.pi) * self.attributes.radius
),
int(
self.y+math.cos((-a/self.attributes.period)
* 2*math.pi) * self.attributes.radius
)
)
)
class LemniscateFormation(EnemyFormation):
"""An 'infinity' shape enemy formation"""
def __init__(self, game: Game, image_name,
attributes: CircleFormationAttributes):
"""Initialise the formation
:param game: The game which this belongs to
:type game: Game
:param image_name: The name of the image to use for the enemy
:param attributes: The attributes to use for spawned enemies
:type attributes: CircleFormationAttributes
"""
super().__init__(game, image_name, attributes)
self.attributes: CircleFormationAttributes
def create_enemies(self):
"""Spawn all enemies"""
for i in range(self.attributes.count):
self.spawn_enemy((0, 0, (i / self.attributes.count)
* self.attributes.period * 0.25))
def position_enemy(self, enemy: FormationEnemy):
"""Position an enemy
:param enemy:
:type enemy: FormationEnemy
"""
a = enemy.offset_a + self.game.alpha
t = (-a/self.attributes.period)*2*math.pi
x = self.x+(self.attributes.radius * math.cos(t)) / \
(1 + math.sin(t)**2)
y = self.y+(self.attributes.radius * math.sin(t) * math.cos(t)) / \
(1 + math.sin(t)**2)
enemy.set_pos(
(
int(x),
int(y)
)
)
@dataclass
class TriangleFormationAttributes(FormationAttributes):
"""Attributes for a triangular formation"""
spacing: int = 16
class TriangleFormation(EnemyFormation):
"""A v-shaped formation of enemies"""
def __init__(self, game: Game, image_name: str,
attributes: TriangleFormationAttributes):
"""Initialise the formation
:param game: The game which this belongs to
:type game: Game
:param image_name: The name of the image to use for the enemy
:type image_name: str
:param attributes: The attributes to use for spawned enemies
:type attributes: TriangleFormationAttributes
"""
super().__init__(game, image_name, attributes)
self.attributes: TriangleFormationAttributes
def create_enemies(self):
"""Spawn all enemies in this formation"""
for i in range(self.attributes.count):
y = -((i+1) // 2)*self.attributes.spacing//2
# first part is multiply by 1 or -1 to determine the side
# then just have an offset for how far
x = 2*((i % 2)-0.5) * ((i+1)//2)*self.attributes.spacing
self.spawn_enemy((x, y, 1))
@dataclass
class RectangleFormationAttributes(TriangleFormationAttributes):
"""Attributes for a rectangle formation"""
width: int = 5
height: int = 2
class RectangleFormation(EnemyFormation):
"""A grid-like formation of enemies"""
def __init__(self, game: Game, image_name,
attributes: RectangleFormationAttributes):
"""Initialise the formation
:param game: The game which this belongs to
:type game: Game
:param image_name: The name of the image to use for the enemy
:param attributes: The attributes to use for spawned enemies
:type attributes: RectangleFormationAttributes
"""
super().__init__(game, image_name, attributes)
self.attributes: RectangleFormationAttributes
def create_enemies(self):
"""Spawn all enemies"""
full_width = self.attributes.width * self.attributes.spacing
full_height = self.attributes.height * self.attributes.spacing
for y in range(self.attributes.height):
offset_y = ((y+0.5)*self.attributes.spacing)-(full_height/2)
for x in range(self.attributes.width):
offset_x = ((x+0.5)*self.attributes.spacing)-(full_width/2)
self.spawn_enemy((offset_x, offset_y, 1))

341
formation_spawner.py Normal file
View file

@ -0,0 +1,341 @@
import math
from random import choice, randint, random
from boss import CircleBossFormation, SnakeBossFormation
from enemy import EnemyAttributes
from formation import (
CircleFormation,
CircleFormationAttributes,
EnemyFormation,
FormationAttributes,
LemniscateFormation,
RectangleFormation,
RectangleFormationAttributes,
TriangleFormation,
TriangleFormationAttributes,
)
def wobble_pattern(formation):
"""A sinusoidal movement pattern
:param formation: Formation to move
"""
x = (1+math.sin(formation.alpha/80)) * formation.game.w/2
y = formation.y + (1 if formation.alpha % 4 == 0 else 0)
formation.set_pos((x, y))
def speed_pattern(formation):
"""Quickly move the formation downwards
:param formation: Formation to move
"""
x = formation.x
y = formation.y + 2
formation.set_pos((x, y))
def slow_pattern(formation):
"""Slowly move the formation downwards
:param formation: Formation to move
"""
x = formation.x
y = formation.y + (1 if formation.alpha % 8 == 0 else 0)
formation.set_pos((x, y))
def slide_in_pattern(formation):
"""Slowly move into the center of the screen and then remain there
:param formation: Formation to move
"""
cy = formation.game.h//3
if formation.alpha < 400:
x = formation.x
y = (formation.alpha/400) * (cy*1.5) - (cy*0.5)
else:
x = formation.x
y = formation.y
formation.set_pos((int(x), int(y)))
def no_pattern(formation):
"""No movement, stay in the center
:param formation: Formation to move
"""
formation.set_pos((
formation.game.w // 2,
formation.game.h // 3
))
def figure_of_eight_pattern(formation):
"""Move the formation in a figure of eight
:param formation: Formation to move
"""
period = 600
edge = 8
radius = formation.game.h//3 - edge
cx, cy = formation.game.w//2, formation.game.h//3
if formation.alpha < 200:
x = formation.x
y = (formation.alpha/200) * (cy*1.5) - (cy*0.5)
else:
a = formation.alpha - 200
t = (a/period)*2*math.pi - math.pi/2
y = cy + (radius * math.cos(t)) / (1 + math.sin(t)**2)
x = cx + (radius * math.sin(t) * math.cos(t)) / (1 + math.sin(t)**2)
formation.set_pos((int(x), int(y)))
class FormationSpawner():
"""Object to manage spawning of enemies and phases"""
def __init__(self, game):
"""Initialise the formation spawner
:param game: The game which this belongs to
"""
self.game = game
self.formations = []
self.difficulty_multiplier = 0.5
self.next_formation = 0
self.phase = -1
self.phases = [
Phase("Phase:1", [
self.spawn_fleet,
self.spawn_loop,
self.spawn_orbital], 10),
Phase("Boss:1", [self.spawn_circle_boss], 1),
Phase("Phase:2", [
self.spawn_fleet,
self.spawn_loop,
self.spawn_orbital], 10, max_wave=3),
Phase("Boss:2", [self.spawn_snake_boss], 1),
]
self.to_spawn = 0
self.current_reward = 1
def tick(self):
"""Update all formations"""
for formation, update in self.formations:
formation.tick(self.game.player)
update(formation)
if formation.y > self.game.h:
formation.destroy()
self.formations = list(
filter(lambda s: not s[0].destroyed, self.formations))
self.spawn_next()
def spawn_random(self):
"""Spawn a random formation"""
options = [
self.spawn_fleet,
self.spawn_loop,
self.spawn_orbital,
self.spawn_rectangle
]
choice(options)()
def spawn_formation(self, formation: EnemyFormation, update):
"""Add a formation to the list of formations
:param formation: Formation to add
:type formation: EnemyFormation
:param update: movement function to use for this formation
"""
update(formation)
formation.show()
self.formations.append((formation, update))
def spawn_circle_boss(self):
"""Spawn the circle boss"""
attributes = EnemyAttributes(
hp=int(15*self.difficulty_multiplier),
reward=self.current_reward,
cooldown=50
)
formation = CircleBossFormation(self.game, attributes)
formation.set_pos((self.game.w//2, 0))
update = figure_of_eight_pattern
self.spawn_formation(formation, update)
def spawn_snake_boss(self):
"""Spawn the snake boss"""
attributes = FormationAttributes(
hp=int(10*self.difficulty_multiplier),
reward=self.current_reward,
cooldown=160
)
formation = SnakeBossFormation(self.game, attributes)
formation.set_pos((self.game.w//2, 0))
update = slide_in_pattern
self.spawn_formation(formation, update)
def spawn_fleet(self):
"""Spawn the fleet formation"""
sprite = randint(6, 7)
position = (random()*self.game.w, -32)
attributes = TriangleFormationAttributes(
hp=int(self.difficulty_multiplier),
cooldown=-1,
reward=self.current_reward,
count=randint(1, 3)*2 + 1,
spacing=8
)
formation = TriangleFormation(
self.game, f"smallenemy{sprite}", attributes)
formation.set_pos(position)
update = speed_pattern
self.spawn_formation(formation, update)
def spawn_orbital(self):
"""Spawn the orbital formation"""
position = (random()*self.game.w, -32)
sprite = choice((1, 3))
attributes = CircleFormationAttributes(
hp=int(self.difficulty_multiplier * 2),
count=randint(3, 4)*2,
radius=randint(10, 20),
period=randint(100//int(self.difficulty_multiplier), 400),
cooldown=80,
reward=self.current_reward
)
formation = CircleFormation(
self.game, f"smallenemy{sprite}", attributes)
formation.set_pos(position)
update = wobble_pattern
formation.alpha = randint(1, 1000)
self.spawn_formation(formation, update)
def spawn_rectangle(self):
"""Spawn the rectangle formation"""
sprite = choice((0, 2))
position = (random() * self.game.w, -32)
attributes = RectangleFormationAttributes(
hp=int(self.difficulty_multiplier * 2),
width=randint(4, 6),
height=randint(2, 3),
cooldown=80,
reward=self.current_reward,
)
formation = RectangleFormation(
self.game, f"smallenemy{sprite}", attributes
)
formation.set_pos(position)
update = wobble_pattern
formation.alpha = randint(1, 1000)
self.spawn_formation(formation, update)
def spawn_loop(self):
"""Spawn the loop formation"""
sprite = choice((4, 5))
position = (random()*self.game.w, -32)
attributes = CircleFormationAttributes(
count=randint(4, 8),
radius=randint(self.game.w//2, self.game.w),
period=randint(200, 300),
hp=int(self.difficulty_multiplier),
reward=self.current_reward,
cooldown=160,
)
formation = LemniscateFormation(
self.game, f"smallenemy{sprite}", attributes)
formation.set_pos(position)
update = slow_pattern
self.spawn_formation(formation, update)
def spawn_next(self):
"""Spawn the next formation to be spawned"""
if self.to_spawn > 0:
if len(self.formations) < self.current_phase().max_wave:
if self.game.alpha > self.next_formation \
and self.next_formation != -1:
self.next_formation = self.game.alpha \
+ 100 / self.difficulty_multiplier
self.current_phase().get_spawn_function()()
self.to_spawn -= 1
else:
if len(self.formations) == 0:
self.next_phase()
def next_phase(self):
"""Increment the phase by 1 and start the next phase"""
self.phase += 1
self.game.save_game()
self.start_phase()
def start_phase(self):
"""Start the next phase"""
self.to_spawn = self.current_phase().duration
self.difficulty_multiplier = (self.phase+2) * 0.5
self.current_reward = int(2**self.difficulty_multiplier)
self.next_formation = self.game.alpha + 100
if self.current_phase().name:
self.game.effect_player.splash_text(self.current_phase().name)
def current_phase(self):
"""Return the current phase"""
if self.phase < len(self.phases):
return self.phases[self.phase]
return Phase(f"Phase:{self.phase-1}", [
self.spawn_random
], 10 * self.difficulty_multiplier,
max_wave=int(self.difficulty_multiplier)
)
def clear_all(self):
"""Remove all formation objects"""
for f, _ in self.formations:
f.destroy()
class Phase:
"""Rules for which formation will be spawned"""
def __init__(self, name, spawn_functions, duration, max_wave=2):
"""__init__.
:param name: The name of the phase
:param spawn_functions: A list of functions to use to spawn enemies
:param duration: The number of formations to spawn
before the phase is over
:param max_wave: The maximum number of formations to spawn at a time
"""
self.spawn_functions = spawn_functions
self.duration = duration
self.name = name
self.max_wave = max_wave
def get_spawn_function(self):
"""Return a random spawn function"""
return choice(self.spawn_functions)

41
frame_counter.py Normal file
View file

@ -0,0 +1,41 @@
from sys import stderr
from time import time
class FrameCounter:
"""Creates a main loop and ensures that the framerate is static"""
def __init__(self, canvas, target_fps):
"""Initialise the frame counter
:param canvas: The canvas which to call after on
:param target_fps: The fps to aim to achieve
"""
self.canvas = canvas
self.fps = target_fps
self.frame_time = 1 / target_fps
self.last_frame = time()
self.current_fps = 1
def next_frame(self, callback):
"""Calculate when the next frame should be called
:param callback: function to call for the next frame
"""
t = time()
ft = t - self.last_frame
delay = 0
if ft > self.frame_time:
if ft - self.frame_time > self.frame_time / 5:
print(
f"Help! Running {ft - self.frame_time} seconds behind!",
file=stderr)
else:
delay = self.frame_time - ft
self.canvas.after(int(delay*1000), callback)
self.current_fps = 1 / (delay+ft)
self.last_frame = t

310
game.py Normal file
View file

@ -0,0 +1,310 @@
from random import randint, random
from tkinter import Canvas, PhotoImage, Tk
from typing import List
from config import Config
from font import Font
from frame_counter import FrameCounter
from inputs import InputController
from sprite import Sprite
from textures import TextureFactory
class Game:
"""A generic game object"""
def __init__(self) -> None:
"""Initialise the game
"""
self.win = Tk()
game_width, game_height = (
Config.WIDTH*Config.SCALE, Config.HEIGHT*Config.SCALE
)
self.w, self.h = Config.WIDTH, Config.HEIGHT
self.win.geometry(f"{game_width}x{game_height}")
self.canvas = Canvas(self.win, width=game_width,
height=game_height, bg="#000")
self.canvas.pack()
self.texture_factory = TextureFactory(scale=Config.SCALE)
self.effect_player = EffectPlayer(self)
self.frame_counter = FrameCounter(self.canvas, Config.FPS)
self.inputs = InputController(self)
self.sprites = []
self.score = 0
self.alpha = 0
def start(self):
"""Start the game"""
self.loop()
self.win.mainloop()
def tick(self):
"""Update the game's sprites"""
for sprite in self.sprites:
sprite.tick()
self.effect_player.tick()
def loop(self):
"""Loop the game at a set framerate"""
self.alpha += 1
self.tick()
self.frame_counter.next_frame(self.loop)
def clear_all(self):
"""Remove all game sprites"""
for sprite in self.sprites:
sprite.destroy()
self.sprites = []
class GameSprite(Sprite):
"""A sprite which belongs to a game"""
def __init__(self, game: Game, image: PhotoImage):
"""Initialise the sprite
:param game: The game which this belongs to
:type game: Game
:param image: The image to use for the sprite
:type image: PhotoImage
"""
self.game = game
super().__init__(game.canvas, image, (0, 0))
def move(self, x, y):
"""Move the sprite by a certain amount
:param x: Amount of pixels to move right
:param y: Amount of pixels to move down
"""
# if the object needs to move less than a pixel
# only move it every few frames to create this effect
if abs(x) >= 1:
self.x += x
elif x != 0:
if self.game.alpha % (1/x) == 0:
self.x += 1
if abs(y) >= 1:
self.y += y
elif y != 0:
if self.game.alpha % (1/y) == 0:
self.y += 1
self.update_position()
class GameEffect(GameSprite):
"""An effect that can be played within game"""
def __init__(self, game: Game, image: PhotoImage,
duration=10, momentum=(0, 0)):
"""Initialise the game effect
:param game: The game which this belongs to
:type game: Game
:param image: The image to use for this
:type image: PhotoImage
:param duration: How long this effect should last for
:param momentum: Which direction to move this effect
"""
self.start_time = game.alpha
self.duration = duration
self.velocity_x, self.velocity_y = momentum
super().__init__(game, image)
def tick(self):
"""Move the effect by its momentum and remove it if its over"""
super().tick()
self.move(self.velocity_x, self.velocity_y)
alpha = self.game.alpha - self.start_time
if self.duration != -1 and alpha > self.duration:
self.destroy()
class AnimatedEffect(GameEffect):
"""An effect which involves animating an image"""
def __init__(self, game: Game, images: List[PhotoImage],
frame_time=1, momentum=(0, 0)):
"""Initialise the effect
:param game: The game which this belongs to
:type game: Game
:param images: The images to use for this animation
:type images: List[PhotoImage]
:param frame_time: Length of each frame of the animation
:param momentum: Direction to move this effect
"""
self.start_time = game.alpha
self.frame_time = frame_time
self.images = images
self.frame_start = game.alpha
super().__init__(game, images[0], duration=len(
images)*frame_time, momentum=momentum)
def tick(self):
"""Animate the effect"""
super().tick()
alpha = self.game.alpha - self.start_time
i = int(alpha // self.frame_time)
if i < len(self.images):
self.set_image(self.images[i])
else:
self.destroy()
class EffectPlayer:
"""An object which concerns itself with managing the effects"""
def __init__(self, game: Game) -> None:
"""Initialise the
:param game: The game which this belongs to
:type game: Game
"""
self.sprites = []
self.game = game
self.explosion_frames = []
self.star_image: PhotoImage
def load_textures(self):
"""Load effect textures"""
self.explosion_frames = [
self.game.texture_factory.get_image(f"explosion{i+1}")
for i in range(3)
]
self.star_image = self.game.texture_factory.get_image("star")
def tick(self):
"""Update all effects"""
for sprite in self.sprites:
sprite.tick()
self.sprites = Sprite.remove_destroyed(self.sprites)
def create_stars(self):
"""Initialise the stars in the background"""
for _ in range(100):
self.create_star(True)
def create_star(self, new=False):
"""Add a star to the background
:param new: Whether this star should be added at
the top of the screen or anywhere
"""
x = randint(0, self.game.w)
if new:
y = randint(0, self.game.h)
else:
y = -1
speed = randint(1, 4) * 0.1
duration = 2*self.game.h / speed
star = GameEffect(
self.game,
self.star_image,
duration=int(duration),
momentum=(0, speed)
)
star.set_pos((x, y))
star.send_to_back()
star.show()
self.sprites.append(star)
def create_explosion(self, position=(0, 0)):
"""Create an explosion effect
:param position: location of the explosion
"""
for _ in range(randint(1, 3)):
m = ((random()*2)-1, (random()*2)-1)
explosion_sprite = AnimatedEffect(
self.game, self.explosion_frames, frame_time=5, momentum=m)
explosion_sprite.set_pos(position)
explosion_sprite.show()
self.sprites.append(explosion_sprite)
def splash_text(self, text, duration=50):
"""splash_text.
:param text:
:param duration:
"""
text_img = Font.load_text(self.game.texture_factory, text)
position = (
(self.game.w-Font.FONT_WIDTH*len(text)) // 2,
(self.game.h-Font.FONT_SIZE) // 3
)
text_sprite = GameEffect(
self.game, text_img, duration=duration)
text_sprite.set_pos(position)
text_sprite.show()
self.sprites.append(text_sprite)
class DamageableSprite(GameSprite):
"""Sprite with health points """
def __init__(self, game: Game, image_name: str, hp=3):
"""Initialise the sprite
:param game: The game which this belongs to
:type game: Game
:param image_name: The name of the image to use for this sprite
:type image_name: str
:param hp: The number of hit points this sprite spawns with
"""
self.image = game.texture_factory.get_image(image_name)
self.white_image = game.texture_factory.get_image(
f"{image_name}:white")
self.hp = hp
self.animation_frame = 0
super().__init__(game, self.image)
def damage(self, amount=1):
"""Decrease number of hit points by an amount
:param amount:
"""
if not self.destroyed:
self.hp -= amount
self.animation_frame = 5
if self.hp <= 0:
self.hp = 0
self.destroy()
self.game.effect_player.create_explosion(self.get_pos())
def tick(self):
"""Update the sprite"""
super().tick()
if self.animation_frame > 0:
self.animation_frame -= 1
if self.animation_frame % 2 == 0:
self.set_image(self.image)
else:
self.set_image(self.white_image)
if __name__ == "__main__":
print("!!!")
print("This is not the main file!")
print("Pleae run\n\tpython main.py\ninstead!")
print("!!!")

152
hud.py Normal file
View file

@ -0,0 +1,152 @@
from game import Game, GameSprite
from font import Font
class ScoreCounterSprite(GameSprite):
"""Single digit for a score counter"""
def __init__(self, game: Game):
"""Initialise the score counter
:param game: The game which this belongs to
:type game: Game
"""
self.number_images = []
self.x = 0
for i in range(10):
self.number_images.append(
Font.load_text(game.texture_factory, str(i)))
super().__init__(game, self.number_images[0])
def update_image(self):
"""Update the digit"""
self.set_image(self.number_images[int(self.x % 10)])
def set(self, x):
"""Set the image
:param x: number to set this digit to
"""
self.x = x
self.update_image()
class ScoreCounter:
"""Sprite to display a number"""
def __init__(self, game: Game, num_digits, position=(0, 0)) -> None:
"""__init__.
:param game:
:type game: Game
:param num_digits:
:param position:
:rtype: None
"""
self.digits = []
x, y = position
self.number = 0
for i in range(num_digits):
sprite = ScoreCounterSprite(game)
sprite.set_pos((x+(Font.FONT_SIZE + 1)*i, y))
self.digits.append(sprite)
def set(self, number):
"""Set the score to be displayed
:param number:
"""
if number != self.number:
self.number = number
power = 10**len(self.digits)
for digit in self.digits:
power /= 10
digit.set((number // power) % 10)
def destroy(self):
"""Remove this counter"""
for n in self.digits:
n.destroy()
def send_to_front(self):
"""Move this counter to the foreground"""
for d in self.digits:
d.send_to_front()
def show(self):
"""Make this counter visible"""
for d in self.digits:
d.show()
def hide(self):
"""Make this counter invisible"""
for d in self.digits:
d.hide()
class GameHud:
"""Object to manage the items visible in the game's heads up display"""
SCORE_DIGITS = 8
HP_DIGITS = 2
def __init__(self, game) -> None:
"""Initialise the HUD
:param game: The game which this belongs to
"""
self.game = game
self.score_counter = ScoreCounter(game, GameHud.SCORE_DIGITS,
position=(
game.w
- GameHud.SCORE_DIGITS
* (Font.FONT_SIZE+1),
1)
)
self.hp_symbol = GameSprite(game, game.player.image)
self.hp_symbol.set_pos((1, 1))
x_image = Font.load_text(game.texture_factory, "x")
self.x_symbol = GameSprite(game, x_image)
self.x_symbol.set_pos((self.hp_symbol.x+self.hp_symbol.w+1, 1))
self.hp_counter = ScoreCounter(game, GameHud.HP_DIGITS,
position=(self.x_symbol.x+1 +
self.x_symbol.w, 1)
)
self.items = (self.score_counter,
self.hp_symbol,
self.x_symbol,
self.hp_counter)
def tick(self):
"""Update the hud"""
self.score_counter.set(self.game.score)
self.hp_counter.set(
self.game.player.hp if self.game.player.hp > 0 else 0)
for x in self.items:
x.send_to_front()
def destroy(self):
"""Remove all the associated objects"""
for x in self.items:
x.destroy()
def hide(self):
"""Make this object invisible"""
for x in self.items:
x.hide()
def show(self):
"""Make this object visible"""
for x in self.items:
x.show()

133
inputs.py Normal file
View file

@ -0,0 +1,133 @@
from dataclasses import dataclass
from os import path
from sys import stderr
from config import Config
@dataclass
class InputSettings:
"""Settings for keybinds"""
down: str = "Down"
left: str = "Left"
up: str = "Up"
right: str = "Right"
action: str = "space"
pause: str = "Escape"
boss: str = "F9"
def save_inputs(self):
"""Save keybinds to a file"""
with open(Config.SETTINGS_FILE, "w", encoding="utf-8") as file:
for key, value in vars(self).items():
file.write(f"{key}: {value}\n")
def load_inputs(self):
"""Load keybinds from a file"""
if path.exists(Config.SETTINGS_FILE):
with open(Config.SETTINGS_FILE, "r", encoding="utf-8") as file:
for line in file.readlines():
split = line.strip().split(": ")
if len(split) == 2:
setattr(self, split[0], split[1])
else:
print(
f"Settings file corrupted? Invalid line {line}",
file=stderr
)
class InputController:
"""Object which listens to key inputs"""
def __init__(self, game) -> None:
"""Initialise the input controller
:param game: The game which this belongs to
:rtype: None
"""
game.win.bind('<KeyPress>', self.on_key_press)
game.win.bind('<KeyRelease>', self.on_key_release)
self.handlers = []
self.settings = InputSettings()
self.settings.load_inputs()
self.k_down = False
self.k_left = False
self.k_up = False
self.k_right = False
self.k_action = False
self.k_pause = False
self.k_boss = False
def on_key_press(self, e):
"""Handle Key press events
:param e: The key press event to handle
"""
if e.keysym == self.settings.left:
self.k_left = True
if e.keysym == self.settings.right:
self.k_right = True
if e.keysym == self.settings.up:
self.k_up = True
if e.keysym == self.settings.down:
self.k_down = True
if e.keysym == self.settings.action:
self.k_action = True
if e.keysym == self.settings.pause:
self.k_pause = True
for t, h in self.handlers:
if t == "press" and h(e):
break
def on_key_release(self, e):
"""Handle Key release events
:param e: The key press event to handle
"""
if e.keysym == self.settings.left:
self.k_left = False
if e.keysym == self.settings.right:
self.k_right = False
if e.keysym == self.settings.up:
self.k_up = False
if e.keysym == self.settings.down:
self.k_down = False
if e.keysym == self.settings.action:
self.k_action = False
if e.keysym == self.settings.pause:
self.k_pause = False
for t, h in self.handlers:
if t == "release" and h(e):
break
def add_keypress_handler(self, callback):
"""Register a key press listener
:param callback:
"""
self.handlers.insert(0, ("press", callback))
def add_keyrelease_handler(self, callback):
"""Register a key release listener
:param callback:
"""
self.handlers.insert(0, ("release", callback))

443
leaderboard.py Normal file
View file

@ -0,0 +1,443 @@
from os import path
from random import randint
from typing import List
from config import Config
from font import Font
from game import Game, GameSprite
class LeaderboardFile:
"""Object to manage saving and loading the leaderboard"""
def __init__(self):
"""Initialise the leaderboard file"""
self.entries = []
def load_entries(self):
"""Load leaderboard entries from a file"""
self.entries = []
if path.exists(Config.LEADERBOARD_FILE):
with open(Config.LEADERBOARD_FILE, "rb") as file:
while (entry := file.read(8 + Config.NICK_LEN)):
name = entry[0:Config.NICK_LEN]
score = entry[Config.NICK_LEN:11]
score_entry = (name.decode("ascii"),
int.from_bytes(score, byteorder="little"))
self.entries.append(score_entry)
self.sort_entries()
def sort_entries(self):
"""Sort leaderboard entries"""
self.entries.sort(key=(lambda e: e[1]))
self.entries.reverse()
def save_entries(self):
"""Save leaderboard entries"""
with open(Config.LEADERBOARD_FILE, "wb") as file:
for name, score in self.entries:
file.write(bytes(name[0:Config.NICK_LEN], "ascii"))
file.write(int(score).to_bytes(8, "little"))
def add_entry(self, name, score):
"""Add a leaderboard entry
:param name: Initials of the player
:param score: The sore that was achieved
"""
self.entries.append((name, score))
self.sort_entries()
class NameEntryLetter(GameSprite):
"""A single sprite used in a initial entry"""
def __init__(self, game: Game, image, letter):
"""Initialise the letter
:param game: The game which this belongs to
:type game: Game
:param image: The image to use for this
:param letter: the letter to use
"""
self.letter = letter
super().__init__(game, image)
class NameEntry():
"""An initial entry element, allowing the user to enter their initials"""
def __init__(self, game: Game, callback, num_letters=3, position=(0, 0)):
"""Initialise the name entry
:param game: The game which this belongs to
:type game: Game
:param callback: callback to call when the entry is complete
:param num_letters: Number of letters to use for the initials
:param position: Position of this element
"""
self.game = game
self.callback = callback
self.alphabet = [
Font.load_text(game.texture_factory, c)
for c in list(map(chr, range(97, 123)))
]
self.letters: List[NameEntryLetter] = []
self.selection = 0
self.hidden = True
self.populate_letters(num_letters)
self.game.inputs.add_keypress_handler(self.on_key)
self.set_pos(position)
def populate_letters(self, num_letters):
"""Create sprites for each of the letters
:param num_letters: Number of letters to use for initials
"""
for _ in range(num_letters):
sprite = NameEntryLetter(
self.game, self.alphabet[0], 0
)
self.letters.append(sprite)
enter_image = Font.load_text(self.game.texture_factory, "enter")
self.button = GameSprite(self.game, enter_image)
self.w = self.button.w + (self.letters[0].w+1)*len(self.letters)
self.h = Font.FONT_SIZE
def on_key(self, _):
"""Handle Key press events
:param _: The key press event to handle
"""
inp = self.game.inputs
if not self.hidden:
if inp.k_action and self.selection == len(self.letters):
self.callback(self.get_string())
return True
if inp.k_left:
self.selection -= 1
self.get_selected_letter().show()
if inp.k_right or inp.k_action:
self.get_selected_letter().show()
self.selection += 1
self.get_selected_letter().hide()
self.selection %= len(self.letters) + 1
if inp.k_up:
self.modify_letter(-1)
if inp.k_down:
self.modify_letter(1)
return True
return False
def get_string(self):
"""Get the initials entered"""
return "".join(map(lambda l: chr(97 + l.letter), self.letters))
def modify_letter(self, amount):
"""Increase or decrease a single character
:param amount: number of letters to increment by
"""
letter = self.get_selected_letter()
if letter in self.letters:
letter.letter += amount
letter.letter %= len(self.alphabet)
self.update_letter(letter)
letter.show()
def update_letter(self, letter):
"""Upare the image of a single letter
:param letter: letter to update
"""
letter.set_image(self.alphabet[letter.letter])
def get_selected_letter(self):
"""Get the letter that has been selected"""
if self.selection < len(self.letters):
return self.letters[self.selection]
return self.button
def set_pos(self, pos):
"""set the element's position
:param pos: position to move to
"""
pos_x, pos_y = pos
offset_x = 0
for letter in self.letters:
letter.set_pos((pos_x + offset_x, pos_y))
offset_x += letter.w + 1
offset_x += Font.FONT_SIZE
self.button.set_pos((pos_x + offset_x, pos_y))
def update_position(self):
"""Update the position of all the letters"""
for letter in self.letters:
letter.update_position()
def show(self):
"""Make this object visible"""
if self.hidden:
for letter in self.letters:
letter.show()
letter.send_to_front()
self.button.show()
self.button.send_to_front()
self.hidden = False
def hide(self):
"""Make this object invisible"""
if not self.hidden:
for letter in self.letters:
letter.hide()
self.button.hide()
self.hidden = True
def tick(self):
"""Update the state of this object"""
if not self.hidden:
selected = self.get_selected_letter()
for letter in self.letters + [self.button]:
if self.game.alpha//15 == self.game.alpha / 15:
if letter == selected:
if (self.game.alpha//15) % 2 == 0:
self.get_selected_letter().show()
else:
self.get_selected_letter().hide()
else:
letter.show()
else:
self.hide()
class Leaderboard:
"""Leaderboard object to display previous scores"""
ANIMATION_TIME = 5
ANIMATION_DELAY = 5
def __init__(self, game: Game):
"""Initialise the leaderboard
:param game: The game which this belongs to
:type game: Game
"""
self.game = game
self.file = LeaderboardFile()
self.entries = []
self.editing = True
self.padding = 5
self.callback = (lambda: None)
self.hidden = True
self.game.inputs.add_keypress_handler(self.on_key)
self.name_entry = NameEntry(self.game, self.submit_name)
self.blinking_sprite = None
self.animation_start = -1
def populate_entries(self, blink_entry=("", 0)):
"""Populate entries.
:param blink_entry:
"""
self.clear_entries()
self.file.load_entries()
editing_area = 0
if self.editing:
editing_area = Font.FONT_SIZE + self.padding*2
remaining_area = self.game.h - self.padding*2 - editing_area
to_fit = remaining_area // (Font.FONT_SIZE+self.padding) - 1
to_draw = self.file.entries[0:to_fit]
# create a row variable that is incremented for each entry
y = self.padding
# create the title sprite and increment the row
image = Font.load_text(self.game.texture_factory, "leaderboard")
sprite = GameSprite(self.game, image)
sprite.set_pos((0, y))
self.entries.append(sprite)
x = (self.game.w - sprite.w) // 2
sprite.set_pos((x, y))
y += sprite.h + self.padding
# calculate the number of zeros to pad the score by
zfill = ((self.game.w-self.padding*2) //
(Font.FONT_SIZE+1)) - Config.NICK_LEN - 5
for name, score in to_draw:
text = f"{name} {str(score).zfill(zfill)}"
x = self.padding
image = Font.load_text(self.game.texture_factory, text)
sprite = GameSprite(self.game, image)
sprite.set_pos((x, y))
if (name, score) == blink_entry:
self.blinking_sprite = sprite
self.entries.append(sprite)
y += sprite.h + self.padding
if self.editing:
self.name_entry.set_pos((self.padding, y+self.padding))
self.name_entry.show()
else:
self.name_entry.hide()
def start_animation(self):
"""Start the animation."""
for e in self.entries:
e.set_pos((-self.game.w, e.y))
self.name_entry.hide()
self.animation_start = self.game.alpha
def on_key(self, _):
"""Handle Key press events
:param _: The key press event to handle
"""
inp = self.game.inputs
if not self.hidden and inp.k_action and not self.editing:
self.callback()
return True
return False
def submit_name(self, name):
"""Submit a name to the leaderboard
:param name:
"""
score = self.game.score
self.file.add_entry(name, score)
self.file.save_entries()
self.editing = False
self.name_entry.hide()
self.populate_entries(blink_entry=(name, score))
self.start_animation()
for e in self.entries:
e.show()
def animate_sprite(self, sprite, i):
"""Animate a single sprite.
:param sprite:
:param i:
"""
alpha = self.game.alpha \
- self.animation_start \
- i*Leaderboard.ANIMATION_DELAY
if alpha <= Leaderboard.ANIMATION_TIME:
if i == 0:
# only title should be h aligned
cx = (self.game.w - sprite.w) // 2
else:
cx = self.padding
x = (alpha/Leaderboard.ANIMATION_TIME) * \
(cx+self.game.w//2) - self.game.w//2
sprite.set_pos((x, sprite.y))
return False
return True
def tick(self):
"""Update the leaderboard"""
animation_complete = True
for i, sprite in enumerate(self.entries):
sprite.send_to_front()
if not self.animate_sprite(sprite, i):
animation_complete = False
if self.editing:
animation_time = self.game.alpha \
- self.animation_start \
- len(self.entries)*Leaderboard.ANIMATION_DELAY
if animation_complete \
and animation_time > Leaderboard.ANIMATION_TIME:
self.name_entry.show()
self.name_entry.tick()
else:
if self.blinking_sprite is not None:
if (self.game.alpha//15) % 2 == 0:
self.blinking_sprite.show()
else:
self.blinking_sprite.hide()
def show(self):
"""Make this object visible"""
if self.hidden:
for m in self.entries:
m.show()
m.send_to_front()
self.hidden = False
def hide(self):
"""Make this object invisible"""
if not self.hidden:
for m in self.entries:
m.hide()
self.name_entry.hide()
self.hidden = True
def start_editing(self):
"""Allow the user to input a name"""
self.editing = True
self.blinking_sprite = None
def clear_entries(self):
"""Remove all the associated objects"""
for entry in self.entries:
entry.destroy()
self.entries = []
# test to add entries to game leaderboard
if __name__ == "__main__":
lb = LeaderboardFile()
lb.load_entries()
for input_name, input_score in lb.entries:
print(f"{input_name} {input_score}")
while True:
input_name = input("Enter name or leave blank to exit: ")
if input_name:
input_score = input("enter score blank for random: ")
if not input_score:
input_score = randint(1, 999999)
lb.add_entry(input_name, int(input_score))
else:
break
for input_name, input_score in lb.entries:
print(f"{input_name} {input_score}")
lb.save_entries()

13
main.py Executable file
View file

@ -0,0 +1,13 @@
#!/usr/bin/env python3
from shooter_game import ShooterGame
def main():
"""The entry function to the game"""
game = ShooterGame()
game.start()
if __name__ == "__main__":
main()

282
menu.py Normal file
View file

@ -0,0 +1,282 @@
import re
from font import Font
from game import Game, GameSprite
from sprite import Sprite
class MenuItem(GameSprite):
"""A selectable item in a menu"""
def __init__(self, game: Game, text, callback):
"""Initialise the item
:param game: The game which this belongs to
:type game: Game
:param text: Text to display for this item
:param callback: function to call when this item is selected
"""
image = Font.load_text(game.texture_factory, text)
self.text = text
self.callback = callback
super().__init__(game, image)
def set_text(self, text):
"""Update the text of an entry
:param text:
"""
self.text = text
image = Font.load_text(self.game.texture_factory, text)
self.set_image(image)
class Menu():
"""Menu object with selectable entries"""
def __init__(self, game: Game, title) -> None:
"""Initialise the menu object
:param game: The game which this belongs to
:type game: Game
:param title: The title of this menu
"""
self.game = game
self.title = title
self.padding = 5
self.game.inputs.add_keypress_handler(self.on_key)
self.menu_items = []
self.selection = 0
carret_image = Font.load_text(game.texture_factory, ">")
self.carret = GameSprite(self.game, carret_image)
title_image = Font.load_text(game.texture_factory, title)
position = ((self.game.w - len(title)*Font.FONT_WIDTH)//2, 5*2)
self.title = GameSprite(self.game, title_image)
self.title.set_pos(position)
self.hidden = True
self.alpha = 0
def on_key(self, _):
"""Handle Key press events
:param event: The key press event to handle
"""
if not self.hidden:
inp = self.game.inputs
if inp.k_down or inp.k_right:
self.selection += 1
if inp.k_up or inp.k_left:
self.selection -= 1
self.selection %= len(self.menu_items)
self.update_carret()
if inp.k_action:
self.menu_items[self.selection].callback()
return True
return False
def update_carret(self):
"""Move the carret to the correct location"""
selected = self.menu_items[self.selection]
x = selected.x - Font.FONT_SIZE
y = selected.y
self.carret.set_pos((x, y))
def arrange_items(self):
"""Move the menu items to their correct positions"""
cy = self.game.h // 2
max_height = sum(item.h for item in self.menu_items
) + self.padding*(len(self.menu_items)-1)
top = cy - max_height//2
y = top
for item in self.menu_items:
item_x = (self.game.w - item.w) // 2
item_y = y
y += item.h + self.padding
item.set_pos((item_x, item_y))
self.update_carret()
def add_item(self, text: str, callback, index=-1):
"""Add a menu item
:param text: Label of this item
:type text: str
:param callback: Function to call when it is selected
:param index: Where to insert this item
"""
if index == -1:
index = len(self.menu_items)
self.menu_items.insert(index, MenuItem(self.game, text, callback))
self.arrange_items()
def show(self):
"""Make this object visible"""
if self.hidden:
for m in self.menu_items:
m.show()
m.send_to_front()
self.carret.show()
self.carret.send_to_front()
self.title.show()
self.title.send_to_front()
self.hidden = False
def hide(self):
"""Make this object invisible"""
if not self.hidden:
for m in self.menu_items:
m.hide()
self.carret.hide()
self.title.hide()
self.hidden = True
def tick(self):
"""Update this object"""
self.alpha += 1
if not self.hidden:
if (self.alpha//15) % 2 == 0:
self.carret.show()
else:
self.carret.hide()
else:
self.carret.hide()
def has_item(self, text):
"""Return true if matching item is found
:param text: Label text to match
"""
for entry in self.menu_items:
if entry.text == text:
return True
return False
def get_item(self, regex) -> MenuItem:
"""Return an item that is matched
:param regex: regular expression to match item text to
:rtype: MenuItem
"""
for entry in self.menu_items:
if re.match(regex, entry.text):
return entry
return self.menu_items[0]
def edit_item(self, regex, new_text):
"""Edit the text of a menu item
:param regex: Regular expression to use to match the item's text to
:param new_text: Text to replace to
"""
for entry in self.menu_items:
if re.match(regex, entry.text):
entry.set_text(new_text)
self.arrange_items()
def del_item(self, text):
"""Remove an item
:param text: Label text to match
"""
for entry in self.menu_items:
if entry.text == text:
entry.destroy()
self.menu_items = Sprite.remove_destroyed(self.menu_items)
self.arrange_items()
return False
class KeybindsMenu(Menu):
"""A menu for selecting keybinds on"""
def __init__(self, game: Game, title):
"""Initialise the menu
:param game: The game which this belongs to
:type game: Game
:param title: The title of this menu
"""
super().__init__(game, title)
self.key_selecting = ""
image = Font.load_text(game.texture_factory, "press any key")
self.press_key_sprite = GameSprite(self.game, image)
self.press_key_sprite.set_pos(
((self.game.w - self.press_key_sprite.w) // 2, self.game.h // 2))
def on_key(self, event):
"""Handle Key press events
:param event: The key press event to handle
"""
if self.key_selecting:
key = event.keysym
self.set_keybind(self.key_selecting, key)
self.press_key_sprite.hide()
self.show()
return True
return super().on_key(event)
def set_keybind(self, name, key):
"""set_keybind.
:param name:
:param key:
"""
setattr(self.game.inputs.settings, self.key_selecting, key)
self.game.inputs.settings.save_inputs()
self.key_selecting = ""
self.edit_item(f"{name}\\s*<.*>", self.get_label(name, key))
def select_keybind(self, keyname):
"""Allow the user to press a key to decide their keybind
:param keyname:
"""
self.hide()
self.press_key_sprite.show()
self.key_selecting = keyname
def get_set_keybind(self, keyname):
"""Return a function that sets the keybind of a particular keyname
:param keyname:
"""
return lambda: self.select_keybind(keyname)
def get_label(self, name, value):
"""Get a label for a keybind item
:param name:
:param value:
"""
available_width = (self.game.w - Font.FONT_SIZE*2) // Font.FONT_WIDTH
num_spaces = available_width - (len(name) + len(value) + 2) - 1
spaces = " " * num_spaces
return f"{name} {spaces}<{value}>"

88
shooter.py Normal file
View file

@ -0,0 +1,88 @@
from dataclasses import dataclass
from typing import List
from game import DamageableSprite, Game, GameSprite
from sprite import Sprite
class Lazer(GameSprite):
"""Lazer object that is shot by a shooter"""
def __init__(self, game: Game, velocity=-4, color="white"):
"""Initialise the lazer
:param game: The game which this belongs to
:type game: Game
:param velocity: Velocity to move the lazer at
:param color: name of the colour of the lazer
"""
self.velocity = velocity
self.game = game
super().__init__(game, game.texture_factory.get_image(
f"lazer:{color}"))
def tick(self):
"""Update this object"""
self.move(0, self.velocity)
if self.y + self.h > self.game.h or self.y < 0:
self.destroy()
@dataclass
class ShooterAttributes:
"""Attributes for a shooter object"""
lazer_color: str = "white"
cooldown: int = 40
velocity: int = 1
hp: int = 3
class Shooter(DamageableSprite):
"""A game object that is able to shoot lazers"""
def __init__(self, game: Game,
image_name: str, attributes: ShooterAttributes):
"""Initialise the shooter
:param game: The game which this belongs to
:type game: Game
:param image_name: The name of the image to use for this sprite
:type image_name: str
:param attributes: The attributes to use for this object
:type attributes: ShooterAttributes
"""
super().__init__(game, image_name, hp=attributes.hp)
self.lazers: List[Lazer] = []
self.attributes = attributes
self.last_shot = self.game.alpha
def shoot(self):
"""Soot a lazer if possible"""
next_shot = self.last_shot + self.attributes.cooldown
if not self.destroyed \
and self.game.alpha > next_shot:
self.last_shot = self.game.alpha
lazer = Lazer(self.game,
velocity=self.attributes.velocity,
color=self.attributes.lazer_color)
lazer.set_pos((self.x + self.w//2 - 1, self.y +
self.h//2 - 1))
lazer.show()
self.lazers.append(lazer)
def tick(self):
"""Update this object"""
super().tick()
for lazer in self.lazers:
lazer.tick()
self.lazers = Sprite.remove_destroyed(self.lazers)
def destroy(self):
"""Remove all the associated objects"""
super().destroy()
for lazer in self.lazers:
self.game.sprites.append(lazer)

363
shooter_game.py Normal file
View file

@ -0,0 +1,363 @@
from enum import Enum, auto
from os import path, remove
from random import random
from boss_key import BossKey
from cheat_engine import Cheat, CheatEngine, DevModeCheat, InvincibilityCheat
from config import Config
from formation_spawner import FormationSpawner
from game import Game
from hud import GameHud
from leaderboard import Leaderboard
from menu import KeybindsMenu, Menu
from shooter import Shooter, ShooterAttributes
from textures import Textures
class GameState(Enum):
"""Enum of possible game states"""
MAIN_MENU = auto()
GAME = auto()
PAUSED = auto()
END_LEADERBOARD = auto()
LEADERBOARD = auto()
SETTINGS = auto()
class GameSave:
"""Static class for saving and loading game"""
@staticmethod
def save_game(game):
"""Save game state to a file
:param game: Game to save
"""
phase = int.to_bytes(game.formation_spawner.phase, 2, "big")
hp = int.to_bytes(game.player.hp, 1, "big")
score = int.to_bytes(game.score, 8, "big")
with open(Config.SAVE_FILE, "wb") as file:
file.write(phase)
file.write(hp)
file.write(score)
if not game.menu.has_item("Continue"):
game.menu.add_item("Continue", game.restore_game, index=0)
@staticmethod
def load_game(game):
"""load game state from file
:param game: Game to load
"""
with open(Config.SAVE_FILE, "rb") as file:
game.formation_spawner.phase = int.from_bytes(file.read(2), "big")
game.player.hp = int.from_bytes(file.read(1), "big")
game.score = int.from_bytes(file.read(8), "big")
@staticmethod
def remove_save(game):
"""Remove the game save file
:param game:
"""
if path.exists(Config.SAVE_FILE):
remove(Config.SAVE_FILE)
if game.menu.has_item("Continue"):
game.menu.del_item("Continue")
class Player(Shooter):
"""Controllable player object"""
def __init__(self, game: Game):
"""Initialise the player
:param game: The game which this belongs to
:type game: Game
"""
attributes = ShooterAttributes(
cooldown=12,
velocity=-2,
hp=10
)
super().__init__(game, "ship", attributes)
self.set_pos(
((self.game.w - self.w) // 2, (self.game.h - self.h)))
def tick(self):
"""Update this object"""
super().tick()
if self.game.inputs.k_left:
self.move(-1, 0)
if self.game.inputs.k_right:
self.move(1, 0)
if self.game.inputs.k_up:
self.move(0, -1)
if self.game.inputs.k_down:
self.move(0, 1)
# clamp the player to the screen
if self.x < 0:
self.set_pos((0, self.y))
if self.y < 0:
self.set_pos((self.x, 0))
if self.x + self.w > self.game.w:
self.set_pos((self.game.w - self.w, self.y))
if self.y + self.h > self.game.h:
self.set_pos((self.x, self.game.h - self.h))
if self.game.inputs.k_action:
self.shoot()
class ShooterGame(Game):
"""Game with menus and enemies to be shot at """
def __init__(self):
"""Initialise the game"""
super().__init__()
self.state = GameState.MAIN_MENU
self.death_time = -1
self.paused_frame = 0
# load textures
Textures.load_textures(self.texture_factory)
self.effect_player.load_textures()
self.effect_player.create_stars()
self.state = GameState.MAIN_MENU
self.formation_spawner = FormationSpawner(self)
self.player = Player(self)
# make the game hud last to make sure its ontop
self.game_hud = GameHud(self)
# create the leaderboard sprites
self.leaderboard = Leaderboard(self)
self.leaderboard.callback = self.show_menu
# make the settings menu
self.settings_menu = KeybindsMenu(self, "Keybinds")
for name, value in vars(self.inputs.settings).items():
label = self.settings_menu.get_label(name, value)
self.settings_menu.add_item(
label,
self.settings_menu.get_set_keybind(name)
)
self.settings_menu.add_item("Return", self.show_menu)
# make the main menu
self.menu = Menu(self, "Main Menu")
if path.exists(Config.SAVE_FILE):
self.menu.add_item("Continue", self.restore_game)
self.menu.add_item("New Game", self.start_game)
self.menu.add_item("Leaderboard", self.show_leaderboard)
self.menu.add_item("Settings", self.show_settings)
self.menu.show()
# make the pause menu
self.pause_menu = Menu(self, "Game Paused")
self.pause_menu.add_item("Resume", self.resume_game)
self.pause_menu.add_item("Save", lambda: (
self.save_game(),
self.effect_player.splash_text("Game saved"),
self.resume_game())
)
self.pause_menu.add_item("Exit", self.show_menu)
# initialise cheats
self.cheat_engine = CheatEngine(self)
self.cheat_engine.add_cheat(
Cheat(self, list("test"),
(lambda: self.effect_player.splash_text("test ok"))
))
self.cheat_engine.add_cheat(
Cheat(self, [
"Up",
"Up",
"Down",
"Down",
"Left",
"Right",
"Left",
"Right",
"b",
"a",
],
(lambda: [self.formation_spawner.spawn_rectangle()
for _ in range(20)])
))
self.cheat_engine.add_cheat(DevModeCheat(self, [
"Left",
"Right",
"Left",
"Right",
"Escape",
"d",
"Up",
"t",
"b",
"b",
"a",
"b",
"s"
]))
self.cheat_engine.add_cheat(InvincibilityCheat(self, list("xyzzy")))
self.boss_key = BossKey(self, self.pause_game)
def tick(self):
"""Update the game state"""
if self.state != GameState.PAUSED:
super().tick()
if random() > 0.9:
self.effect_player.create_star()
if self.state == GameState.MAIN_MENU:
self.menu.tick()
elif self.state == GameState.SETTINGS:
self.settings_menu.tick()
elif self.state == GameState.GAME:
self.tick_game()
elif self.state == GameState.PAUSED:
self.alpha = self.paused_frame
self.pause_menu.tick()
elif self.state == GameState.END_LEADERBOARD:
self.leaderboard.tick()
elif self.state == GameState.LEADERBOARD:
self.leaderboard.tick()
def tick_game(self):
"""Update the game during game play"""
self.game_hud.tick()
self.formation_spawner.tick()
self.player.tick()
if self.player.destroyed:
if self.death_time == -1:
self.death_time = self.alpha
self.effect_player.splash_text("GAME OVER", 100)
elif self.alpha - self.death_time > 100:
self.show_score()
if self.inputs.k_pause:
self.pause_game()
def pause_game(self):
"""Set the game to paused state"""
if self.state == GameState.GAME:
self.state = GameState.PAUSED
self.paused_frame = self.alpha
self.pause_menu.show()
def resume_game(self):
"""Resume the game from paused state"""
self.state = GameState.GAME
self.pause_menu.hide()
def start_game(self):
"""Start a new game"""
self.state = GameState.GAME
GameSave.remove_save(self)
self.menu.hide()
self.pause_menu.hide()
self.formation_spawner.phase = -1
self.clear_all()
self.score = 0
self.player = Player(self)
self.formation_spawner.next_phase()
self.player.show()
self.game_hud.show()
self.death_time = -1
def show_leaderboard(self):
"""Show the game's leaderboard"""
self.state = GameState.LEADERBOARD
self.menu.hide()
self.pause_menu.hide()
self.leaderboard.editing = False
self.leaderboard.populate_entries()
self.leaderboard.start_animation()
self.leaderboard.show()
def show_score(self):
"""Allow the user to enter their name into the leaderboard"""
self.state = GameState.END_LEADERBOARD
self.clear_all()
self.game_hud.hide()
self.leaderboard.editing = True
self.leaderboard.populate_entries()
self.leaderboard.start_animation()
self.leaderboard.show()
GameSave.remove_save(self)
def show_menu(self):
"""Show the main menu"""
self.state = GameState.MAIN_MENU
self.clear_all()
self.leaderboard.hide()
self.game_hud.hide()
self.player.hide()
self.pause_menu.hide()
self.settings_menu.hide()
self.menu.show()
def clear_all(self):
"""Remove all the associated game objects"""
self.formation_spawner.clear_all()
self.player.destroy()
def restore_game(self):
"""Restore the game's state from file"""
self.state = GameState.GAME
self.menu.hide()
self.pause_menu.hide()
self.clear_all()
self.player = Player(self)
self.death_time = -1
GameSave.load_game(self)
self.formation_spawner.start_phase()
self.game_hud.show()
self.player.show()
def save_game(self):
"""Save the game's state to a file"""
GameSave.save_game(self)
def show_settings(self):
"""Show the keybind setting menu"""
self.state = GameState.SETTINGS
self.menu.hide()
self.settings_menu.show()

127
sprite.py Normal file
View file

@ -0,0 +1,127 @@
from tkinter import Canvas, NW, PhotoImage
from config import Config
class Sprite:
"""Sprite."""
@staticmethod
def remove_destroyed(sprite_list):
"""Remove all destroyed sprites from a list
:param sprite_list:
:type sprite_list: list[Sprite]
"""
return list(filter(lambda s: not s.destroyed, sprite_list))
def __init__(self, canvas: Canvas, image: PhotoImage, position=(0, 0)):
"""Initialise the sprite class
:param canvas: The canvas to draw the sprites to
:type canvas: Canvas
:param image: The image to be used for the sprite
:type image: PhotoImage
:param position: The default position to place the sprite
"""
# set positions
self.x, self.y = position
self.canvas = canvas
self.canvas_image = canvas.create_image(
self.x * Config.SCALE, self.y * Config.SCALE,
anchor=NW, image=image, state="hidden")
# get pixel width and heigh ignoring scale
self.w = image.width() // Config.SCALE
self.h = image.height() // Config.SCALE
self.destroyed = False
self.hide()
def update_position(self):
"""Move the image to the sprites position"""
self.canvas.coords(self.canvas_image, self.x *
Config.SCALE, self.y*Config.SCALE)
def set_pos(self, pos):
"""Set the player position
:param pos: Position to move to
"""
self.x, self.y = pos
self.update_position()
def get_pos(self):
"""Return the current position of the sprite"""
return (self.x, self.y)
def move(self, x, y):
"""Move the sprite by x and y
:param x: the number of pixels right to move
:param y: the number of pixels down to move
"""
self.x += x
self.y += y
self.update_position()
def collides(self, other):
"""Check if the sprite collides with another sprite
:param other: The other sprite
"""
return self.x < other.x + other.w \
and self.x + self.w > other.x \
and self.y < other.y + other.h \
and self.h + self.y > other.y
def collide_all(self, others):
"""Check if the sprite collides with a list of sprites
:param others: Array of other sprites to check if collides with
:returns: index of the sprite that it collided with first
or -1 if not colliding
"""
for i, other in enumerate(others):
if self.collides(other):
return i
return -1
def tick(self):
"""Update the sprite"""
def destroy(self):
"""Remove the image from the canvas"""
self.canvas.delete(self.canvas_image)
self.destroyed = True
def send_to_front(self):
"""Move the sprite to the foreground"""
self.canvas.tag_raise(self.canvas_image)
def send_to_back(self):
"""Move the sprite to the background"""
self.canvas.tag_lower(self.canvas_image)
def set_image(self, image: PhotoImage):
"""Change the image used by the sprite
:param image: the image to set the sprite to
:type image: PhotoImage
"""
self.canvas.itemconfig(self.canvas_image, image=image)
def show(self):
"""Set the sprite to be shown"""
self.canvas.itemconfig(self.canvas_image, state="normal")
return self
def hide(self):
"""Set the sprite to be hidden"""
self.canvas.itemconfig(self.canvas_image, state="hidden")
return self
def is_hidden(self):
"""Return True if the sprite is hidden"""
return self.canvas.itemcget(self.canvas_image, "state") == "hidden"

406
textures.py Normal file
View file

@ -0,0 +1,406 @@
from tkinter import PhotoImage
# tell pylint to ignore long lines in this file, since they make more sense
# to not be linewrapped
#
# ignore a particular guideline "when applying the guideline would make the code less readable"
# https://peps.python.org/pep-0008
#
# pylint: disable=line-too-long
class Textures:
"""Static class containing game textures"""
STAR = [
["#AAAAAA"]
]
ENEMY = [
[
[None, "#00E436", "#008751", "#008751", None, None, None, ],
["#00E436", "#00E436", "#00E436", None, None, "#008751", None, ],
["#008751", "#00E436", "#00E436", None, None, "#00E436", "#008751",],
[None, "#008751", "#008751", None, "#00E436", "#00E436", "#008751",],
[None, None, None, "#00E436", "#00E436", "#00E436", "#00E436",],
[None, None, "#00E436", "#00E436", "#1D2B53", "#00E436", "#008751",],
[None, None, "#008751", "#00E436", "#1D2B53", "#1D2B53", "#00E436",],
[None, None, "#008751", "#008751", "#00E436", "#FF004D", "#00E436",],
[None, "#1D2B53", None, "#008751", "#008751", "#00E436", "#00E436",],
[None, "#008751", "#00E436", None, "#008751", "#1D2B53", "#1D2B53",],
["#008751", "#00E436", "#00E436", "#00E436", None, "#1D2B53", "#008751",],
["#00E436", "#00E436", "#00E436", None, None, None, None, ],
["#008751", "#00E436", "#00E436", "#00E436", "#008751", None, None, ],
["#1D2B53", "#008751", "#00E436", "#008751", None, None, None, ],
],
[
[None, None, "#C2C3C7", "#1D2B53", None, None, None, ],
[None, None, "#83769C", "#C2C3C7", "#1D2B53", None, None, ],
[None, None, None, "#83769C", "#C2C3C7", "#83769C", "#C2C3C7",],
["#83769C", "#1D2B53", None, None, "#83769C", "#C2C3C7", "#C2C3C7",],
["#83769C", "#83769C", "#83769C", "#1D2B53", "#C2C3C7", "#83769C", "#C2C3C7",],
["#1D2B53", "#C2C3C7", "#83769C", "#83769C", "#83769C", "#7E2553", "#1D2B53",],
["#C2C3C7", "#C2C3C7", "#1D2B53", None, "#C2C3C7", "#7E2553", "#7E2553",],
["#C2C3C7", "#C2C3C7", None, None, "#83769C", "#C2C3C7", "#C2C3C7",],
["#C2C3C7", "#C2C3C7", "#C2C3C7", "#83769C", "#1D2B53", None, None, ],
["#83769C", "#C2C3C7", "#C2C3C7", "#83769C", None, None, None, ],
["#1D2B53", "#C2C3C7", "#83769C", "#1D2B53", None, None, None, ],
[None, "#C2C3C7", "#83769C", "#83769C", None, None, None, ],
[None, "#1D2B53", "#83769C", "#1D2B53", None, None, None, ],
],
[
[None, None, None, None, "#7E2553", "#FFA300", "#FFA300",],
[None, None, "#7E2553", "#FFA300", "#FFA300", "#FFA300", "#FFEC27",],
[None, "#7E2553", "#FFA300", "#FFA300", "#FFEC27", "#FFEC27", "#FFEC27",],
[None, "#FFA300", "#FFA300", "#FFEC27", "#FFA300", "#FFEC27", "#FFEC27",],
["#7E2553", "#FFA300", "#FFEC27", "#FFEC27", "#FFEC27", "#FFA300", "#FFA300",],
["#FFA300", "#FFA300", "#FFEC27", "#FFEC27", "#FFEC27", "#FFEC27", "#FFEC27",],
["#AB5236", "#AB5236", "#000000", "#000000", "#FFA300", "#FFEC27", "#FFEC27",],
["#AB5236", "#AB5236", "#1D2B53", "#FF004D", "#000000", "#FFA300", "#FFEC27",],
["#AB5236", "#FFA300", "#AB5236", "#1D2B53", "#1D2B53", "#AB5236", "#AB5236",],
["#7E2553", "#AB5236", "#FFA300", "#AB5236", "#AB5236", "#FFA300", "#FFA300",],
["#7E2553", "#7E2553", "#AB5236", "#AB5236", "#AB5236", "#AB5236", "#AB5236",],
[None, "#7E2553", "#7E2553", "#7E2553", None, None, None, ],
]
]
ROCK1 = [
[None, None, "#FFA300", "#FFA300", "#FFA300", "#FFA300", "#5F574F", "#1D2B53", None, None, None, None, ],
[None, "#FFA300", "#FFA300", "#5F574F", "#5F574F", "#5F574F", "#5F574F", "#5F574F", "#1D2B53", None, None, None, ],
["#FFA300", "#FFA300", "#5F574F", "#5F574F", "#5F574F", "#5F574F", "#5F574F", "#5F574F", "#5F574F", "#1D2B53", None, None, ],
["#FFA300", "#5F574F", "#1D2B53", "#5F574F", "#5F574F", "#5F574F", "#1D2B53", "#1D2B53", "#5F574F", "#5F574F", "#1D2B53", None, ],
["#5F574F", "#5F574F", "#5F574F", "#FFA300", "#5F574F", "#1D2B53", "#5F574F", "#5F574F", "#FFA300", "#5F574F", "#5F574F", None, ],
["#5F574F", "#5F574F", "#5F574F", "#5F574F", "#5F574F", "#1D2B53", "#5F574F", "#5F574F", "#FFA300", "#5F574F", "#5F574F", "#1D2B53",],
[None, "#5F574F", "#5F574F", "#5F574F", "#5F574F", "#5F574F", "#FFA300", "#FFA300", "#5F574F", "#5F574F", "#5F574F", "#1D2B53",],
[None, "#1D2B53", "#5F574F", "#5F574F", "#5F574F", "#5F574F", "#5F574F", "#5F574F", "#5F574F", "#5F574F", "#1D2B53", "#1D2B53",],
[None, "#1D2B53", "#1D2B53", "#1D2B53", "#5F574F", "#5F574F", "#5F574F", "#5F574F", "#1D2B53", "#1D2B53", "#1D2B53", "#1D2B53",],
[None, None, "#1D2B53", "#1D2B53", "#1D2B53", "#1D2B53", "#1D2B53", "#1D2B53", "#1D2B53", "#1D2B53", "#1D2B53", "#1D2B53",],
[None, None, None, "#1D2B53", "#1D2B53", "#1D2B53", "#1D2B53", "#1D2B53", "#1D2B53", "#1D2B53", "#1D2B53", None, ],
[None, None, None, None, None, None, "#1D2B53", "#1D2B53", "#1D2B53", "#1D2B53", None, None, ],
]
ROCK2 = [
[None, None, None, None, "#FFA300", "#FFA300", "#5F574F", "#FFA300", "#5F574F", None, None, None, ],
[None, None, None, "#FFA300", "#FFA300", "#5F574F", "#5F574F", "#1D2B53", "#5F574F", "#1D2B53", "#1D2B53", None, ],
[None, None, "#FFA300", "#FFA300", "#5F574F", "#5F574F", "#1D2B53", "#5F574F", "#5F574F", "#5F574F", "#1D2B53", "#1D2B53",],
[None, "#FFA300", "#FFA300", "#5F574F", "#5F574F", "#5F574F", "#5F574F", "#5F574F", "#FFA300", "#5F574F", "#5F574F", "#1D2B53",],
[None, "#FFA300", "#5F574F", "#1D2B53", "#1D2B53", "#5F574F", "#5F574F", "#5F574F", "#5F574F", "#5F574F", "#5F574F", "#1D2B53",],
[None, "#5F574F", "#1D2B53", "#5F574F", "#5F574F", "#5F574F", "#5F574F", "#5F574F", "#5F574F", "#5F574F", "#1D2B53", "#1D2B53",],
[None, None, "#1D2B53", "#5F574F", "#5F574F", "#FFA300", "#5F574F", "#5F574F", "#1D2B53", "#1D2B53", "#1D2B53", None, ],
[None, None, None, "#1D2B53", "#1D2B53", "#1D2B53", "#1D2B53", "#1D2B53", "#1D2B53", None, None, None, ],
]
ROCK3 = [
[None, None, None, None, None, None, None, None, None, "#FFA300", "#FFA300", "#5F574F", "#5F574F", "#5F574F", None, None, ],
[None, None, None, None, "#FFA300", "#FFA300", "#FFA300", "#FFA300", "#FFA300", "#5F574F", "#5F574F", "#5F574F", "#5F574F", "#5F574F", "#1D2B53", None, ],
[None, "#FFA300", "#FFA300", "#FFA300", "#5F574F", "#5F574F", "#5F574F", "#5F574F", "#5F574F", "#5F574F", "#5F574F", "#5F574F", "#1D2B53", "#5F574F", "#1D2B53", "#1D2B53",],
["#FFA300", "#FFA300", "#5F574F", "#5F574F", "#5F574F", "#5F574F", "#5F574F", "#5F574F", "#5F574F", "#5F574F", "#5F574F", "#1D2B53", "#5F574F", "#FFA300", "#1D2B53", "#1D2B53",],
["#FFA300", "#5F574F", "#5F574F", "#1D2B53", "#1D2B53", "#5F574F", "#5F574F", "#1D2B53", "#1D2B53", "#1D2B53", "#5F574F", "#5F574F", "#1D2B53", "#1D2B53", "#1D2B53", "#1D2B53",],
["#5F574F", "#5F574F", "#1D2B53", "#5F574F", "#5F574F", "#FFA300", "#1D2B53", "#1D2B53", "#1D2B53", "#1D2B53", "#1D2B53", "#1D2B53", "#1D2B53", "#1D2B53", "#1D2B53", None, ],
["#1D2B53", "#5F574F", "#5F574F", "#1D2B53", "#1D2B53", "#1D2B53", "#1D2B53", None, None, None, None, None, None, None, None, None, ],
[None, "#1D2B53", "#1D2B53", "#1D2B53", "#1D2B53", None, None, None, None, None, None, None, None, None, None, None, ],
]
ROCK4 = [
[None, None, "#5F574F", "#FFA300", "#FFA300", "#5F574F", None, ],
[None, "#5F574F", "#FFA300", "#1D2B53", "#5F574F", "#5F574F", "#1D2B53",],
[None, "#FFA300", "#1D2B53", "#5F574F", "#5F574F", "#1D2B53", "#1D2B53",],
["#FFA300", "#5F574F", "#5F574F", "#5F574F", "#5F574F", "#1D2B53", "#1D2B53",],
["#FFA300", "#5F574F", "#5F574F", "#5F574F", "#1D2B53", "#1D2B53", None, ],
["#5F574F", "#5F574F", "#1D2B53", "#1D2B53", "#1D2B53", None, None, ],
[None, "#1D2B53", "#1D2B53", "#1D2B53", None, None, None, ],
]
ROCK5 = [
[None, "#FFA300", "#5F574F", "#5F574F", "#1D2B53", None, ],
["#FFA300", "#1D2B53", "#5F574F", "#5F574F", "#5F574F", "#1D2B53",],
["#5F574F", "#5F574F", "#5F574F", "#5F574F", "#5F574F", "#1D2B53",],
["#1D2B53", "#5F574F", "#5F574F", "#5F574F", "#1D2B53", None, ],
[None, "#1D2B53", "#1D2B53", "#1D2B53", None, None, ],
]
SHIP = [
[None, None, None, None, None, None, None, None, ],
[None, None, None, "#83769C", "#83769C", None, None, None, ],
[None, "#83769C", "#1D2B53", "#FFF1E8", "#29ADFF", "#1D2B53", "#5F574F", None, ],
["#83769C", "#83769C", "#7E2553", "#FFF1E8", "#29ADFF", "#1D2B53", "#5F574F", "#5F574F",],
["#83769C", "#C2C3C7", "#83769C", "#29ADFF", "#29ADFF", "#1D2B53", "#83769C", "#5F574F",],
["#83769C", "#C2C3C7", "#83769C", "#5F574F", "#1D2B53", "#1D2B53", "#83769C", "#5F574F",],
["#1D2B53", "#83769C", "#5F574F", "#83769C", "#83769C", "#5F574F", "#5F574F", "#1D2B53",],
[None, "#1D2B53", "#1D2B53", None, None, "#1D2B53", "#1D2B53", None, ],
]
UFO = [
[None, None, None, None, None, "#1D2B53", "#29ADFF", "#FFF1E8",],
[None, None, None, None, "#1D2B53", "#29ADFF", "#FFF1E8", "#FFF1E8",],
[None, None, None, "#1D2B53", "#29ADFF", "#29ADFF", "#29ADFF", "#29ADFF",],
[None, "#83769C", "#83769C", "#C2C3C7", "#FFF1E8", "#FFF1E8", "#C2C3C7", "#C2C3C7",],
["#83769C", "#83769C", "#C2C3C7", "#FFF1E8", "#FFF1E8", "#C2C3C7", "#C2C3C7", "#C2C3C7",],
[None, "#7E2553", "#FF004D", "#7E2553", None, None, "#7E2553", "#FF004D",],
]
LAZER = [
["#8F8F8F"],
["#F8F8F8"],
["#F8F8F8"],
["#F8F8F8"],
["#F8F8F8"],
["#8F8F8F"]
]
SMALLENEMY =[
[
[None, "#00E436", "#008751", None, ],
["#00E436", "#008751", None, None, ],
["#008751", "#008751", "#00E436", "#00E436",],
[None, "#00E436", "#1D2B53", "#00E436",],
[None, "#00E436", "#FF004D", "#00E436",],
["#008751", "#00E436", "#008751", "#008751",],
["#00E436", "#1D2B53", None, None, ],
["#008751", "#00E436", "#00E436", None, ],
],
[
[None, "#7E2553", "#FFA300",],
["#7E2553", "#FFA300", "#FFEC27",],
["#AB5236", "#FFA300", "#FFA300",],
["#AB5236", "#1D2B53", "#AB5236",],
["#7E2553", "#AB5236", "#FFA300",],
[None, None, None, ],
["#FFA300", "#AB5236", None, ],
["#AB5236", "#7E2553", None, ],
],
[
["#FF004D", None, "#7E2553", "#FF004D",],
["#7E2553", "#7E2553", "#FF004D", "#FF77A8",],
[None, "#FF004D", "#FF004D", "#FF004D",],
[None, "#7E2553", None, "#7E2553",],
["#7E2553", "#FF004D", "#1D2B53", "#7E2553",],
["#FF004D", "#FF004D", None, "#FF004D",],
["#FF004D", "#7E2553", None, None, ],
["#7E2553", "#FF004D", "#7E2553", None, ],
],
[
[None, "#1D2B53", "#008751",],
[None, "#008751", "#00E436",],
["#1D2B53", "#008751", "#00E436",],
["#1D2B53", "#7E2553", "#008751",],
[None, "#1D2B53", "#008751",],
["#008751", "#1D2B53", None, ],
["#008751", None, None, ],
["#1D2B53", "#008751", None, ],
],
[
[None, None, "#FFA300", "#FFEC27",],
[None, "#FFA300", "#FFEC27", "#FFEC27",],
["#AB5236", "#FFA300", "#1D2B53", "#FFA300",],
["#FFA300", "#FFEC27", "#FFA300", "#FFEC27",],
["#FFEC27", "#FFA300", None, "#FFEC27",],
["#FFA300", None, None, None, ],
["#AB5236", "#FFA300", None, "#AB5236",],
[None, "#AB5236", None, None, ],
],
[
[None, None, "#7E2553", "#FF77A8",],
[None, "#7E2553", "#FF77A8", "#FFCCAA",],
[None, "#FF77A8", "#FFCCAA", "#FFCCAA",],
[None, "#FFCCAA", "#1D2B53", "#FF77A8",],
[None, "#7E2553", "#FF77A8", "#FFCCAA",],
[None, "#FF77A8", "#1D2B53", "#FFCCAA",],
[None, "#FF77A8", "#1D2B53", "#FF77A8",],
[None, "#7E2553", "#FF77A8", "#7E2553",],
],
[
["#1D2B53", "#83769C", "#1D2B53", None, ],
[None, "#1D2B53", "#C2C3C7", None, ],
["#83769C", "#1D2B53", "#83769C", "#C2C3C7",],
["#C2C3C7", "#83769C", "#C2C3C7", "#C2C3C7",],
["#C2C3C7", "#1D2B53", "#7E2553", "#83769C",],
["#C2C3C7", "#1D2B53", "#C2C3C7", "#C2C3C7",],
["#1D2B53", "#83769C", "#1D2B53", None, ],
[None, "#1D2B53", "#83769C", None, ],
],
[
["#1D2B53", None, None, ],
["#29ADFF", None, None, ],
["#29ADFF", "#1D2B53", None, ],
["#1D2B53", "#29ADFF", "#1D2B53",],
["#29ADFF", None, "#29ADFF",],
["#29ADFF", "#29ADFF", None, ],
["#29ADFF", "#29ADFF", "#1D2B53",],
["#29ADFF", "#1D2B53", None, ],
]
]
EXPLOSION =[
[
["#FFEC27", "#FFEC27"],
["#FFF1E8", "#FFEC27"],
["#7E2553", "#7E2553"]
],
[
["#7E2553", "#FFEC27"],
["#FFEC27", "#FFF1E8"],
],
[
[ None, "#7E2553", "#FFEC27", "#FFEC27"],
[ "#7E2553", "#FFEC27", "#FFEC27", "#FFF1E8"],
[ "#FFEC27", "#FFEC27", "#FFF1E8", "#FFF1E8"],
[ "#FFEC27", "#FFF1E8", "#FFF1E8", "#FFF1E8"],
]
]
@staticmethod
def hmirror_texture(texture):
"""Horizontally mirror a texture
:param texture: texture to mirror
"""
return [(row + row[::-1]) for row in texture]
@staticmethod
def vmirror_texture(texture):
"""Vertically mirror a texture
:param texture: texture to mirror
"""
return texture + texture[::-1]
@staticmethod
def recolor(texture, color):
"""recolor a texture
:param texture: texture to recolor
:param color: Color to multiply the texture with
"""
return [[None if col is None else Textures.multiply_colors(col, color) for col in row] for row in texture]
@staticmethod
def multiply_colors(hex1, hex2):
"""Multiply two RGB colours
:param hex1: first colour
:param hex2: second colour
"""
color1 = Textures.hex_to_rgb(hex1)
color2 = Textures.hex_to_rgb(hex2)
return Textures.rgb_to_hex([color1[i] * color2[i] for i in range(len(color1))])
@staticmethod
def hex_to_rgb(value):
"""Convert a hexadecimal colour value to red green and blue
:param value: hex value
"""
value = value.lstrip('#')
length = len(value)
return tuple(int(value[i:i + length // 3], 16)/256 for i in range(0, length, length // 3))
@staticmethod
def rgb_to_hex(value):
"""Convert red green and blue value to a hexadecimal representation
:param value: RGB value
"""
return "#" + "".join(f"{int(v*256):02X}" for v in value)
@staticmethod
def white_texture(texture):
"""Replace all coloured pixels with white
:param texture: Texture to replace on
"""
return [[None if col is None else "#FFFFFF" for col in row] for row in texture]
@staticmethod
def load_textures(texture_factory):
"""Load all textures within this class
:param texture_factory:
"""
texture_factory.load_texture(
"ufo", Textures.hmirror_texture(Textures.UFO))
texture_factory.load_texture("star", Textures.STAR)
texture_factory.load_texture("ship", Textures.SHIP)
texture_factory.load_texture(
"ship:white", Textures.white_texture(Textures.SHIP))
texture_factory.load_texture("rock1", Textures.ROCK1)
texture_factory.load_texture("rock2", Textures.ROCK2)
texture_factory.load_texture("rock3", Textures.ROCK3)
texture_factory.load_texture("rock4", Textures.ROCK4)
texture_factory.load_texture("rock5", Textures.ROCK5)
texture_factory.load_texture(
"lazer:white", Textures.recolor(Textures.LAZER, "#ffffff"))
texture_factory.load_texture(
"lazer:red", Textures.recolor(Textures.LAZER, "#f2aaaa"))
texture_factory.load_texture(
"lazer:yellow", Textures.recolor(Textures.LAZER, "#f2ffaa"))
for i, enemy in enumerate(Textures.SMALLENEMY):
name = f"smallenemy{i}"
texture = Textures.hmirror_texture(enemy)
texture_factory.load_texture(name, texture)
texture_factory.load_texture(
f"{name}:white", Textures.white_texture(texture))
evil_texture = Textures.recolor(texture, "#FF5555")
texture_factory.load_texture(f"{name}_evil", evil_texture)
texture_factory.load_texture(
f"{name}_evil:white", Textures.white_texture(evil_texture))
for i, enemy in enumerate(Textures.ENEMY):
name = f"enemy{i}"
texture_factory.load_texture(name, Textures.hmirror_texture(enemy))
texture_factory.load_texture(
f"{name}:white", Textures.white_texture(Textures.hmirror_texture(enemy)))
texture_factory.load_texture("explosion3", Textures.EXPLOSION[0])
texture_factory.load_texture("explosion2", Textures.hmirror_texture(
Textures.vmirror_texture(Textures.EXPLOSION[1])))
texture_factory.load_texture("explosion1", Textures.hmirror_texture(
Textures.vmirror_texture(Textures.EXPLOSION[2])))
class TextureFactory:
"""Object that deals with loading and scaling textures"""
def __init__(self, scale) -> None:
"""Initialise the texture factory
:param scale: the amount of pixels to upscale by
:rtype: None
"""
self.textures = {}
self.scale = scale
def load_texture(self, namespace, texture_matrix):
"""Load and upscale a texture
:param namespace: namespace to save this texture to
:param texture_matrix: A matrix of hex colours that represents the texture
"""
if namespace not in self.textures:
height = len(texture_matrix) * self.scale
width = len(texture_matrix[0]) * self.scale
photo_image = PhotoImage(width=width, height=height)
for matrix_y, row in enumerate(texture_matrix):
for matrix_x, color in enumerate(row):
if color is not None:
pixel_string = (
"{" + f"{color} "*self.scale + "} ") * self.scale
photo_image.put(
pixel_string, (matrix_x*self.scale, matrix_y*self.scale))
self.textures[namespace] = photo_image
return photo_image
return self.get_image(namespace)
def get_image(self, namespace):
"""Get a loaded image
:param namespace: to load the image from
"""
if namespace not in self.textures:
raise Exception(
f"Provided namespace \"{namespace}\" has not been loaded!")
return self.textures[namespace]