mirror of
https://git.davidovski.xyz/tkinter-space-game.git
synced 2024-08-15 00:43:41 +00:00
444 lines
13 KiB
Python
444 lines
13 KiB
Python
|
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()
|