From b99dbb396c313f4b3130b566c0df42c10eec6084 Mon Sep 17 00:00:00 2001 From: davidovski Date: Thu, 5 Jan 2023 11:35:28 +0000 Subject: Initial Commit --- leaderboard.py | 443 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 443 insertions(+) create mode 100644 leaderboard.py (limited to 'leaderboard.py') diff --git a/leaderboard.py b/leaderboard.py new file mode 100644 index 0000000..7abcf91 --- /dev/null +++ b/leaderboard.py @@ -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() -- cgit v1.2.1