summaryrefslogtreecommitdiff
path: root/leaderboard.py
diff options
context:
space:
mode:
Diffstat (limited to 'leaderboard.py')
-rw-r--r--leaderboard.py443
1 files changed, 443 insertions, 0 deletions
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()