tkinter-space-game/leaderboard.py
2023-01-05 11:35:28 +00:00

443 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()