Compare commits

...

10 Commits

41 changed files with 269 additions and 111 deletions

1
.gitignore vendored
View File

@@ -1,3 +1,4 @@
*.dem
__pycache__/
venv
.venv

Binary file not shown.

View File

Before

Width:  |  Height:  |  Size: 7.2 KiB

After

Width:  |  Height:  |  Size: 7.2 KiB

BIN
assets/images/bomb.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 377 KiB

View File

Before

Width:  |  Height:  |  Size: 3.1 KiB

After

Width:  |  Height:  |  Size: 3.1 KiB

View File

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 10 KiB

View File

Before

Width:  |  Height:  |  Size: 9.4 KiB

After

Width:  |  Height:  |  Size: 9.4 KiB

View File

Before

Width:  |  Height:  |  Size: 19 KiB

After

Width:  |  Height:  |  Size: 19 KiB

View File

@@ -1,6 +1,6 @@
{
"material": "maps/cs_office.png",
"material": "assets/maps/overview/cs_office.png",
"pos_x": -1838,
"pos_y": 1858,
"scale": 4.1,

View File

@@ -0,0 +1,8 @@
{
"material": "assets/maps/overview/de_ancient.png",
"pos_x": -2953,
"pos_y": 2164,
"scale": 5
}

View File

@@ -1,5 +1,5 @@
{
"material": "maps/de_anubis.png",
"material": "assets/maps/overview/de_anubis.png",
"pos_x": -2796.000000,
"pos_y": 3328.000000,
"scale": 5.220000

View File

@@ -1,6 +1,6 @@
{
"material": "maps/de_dust2.png" ,
"material": "assets/maps/overview/de_dust2.png" ,
"pos_x": -2476 ,
"pos_y": 3239,
"scale": 4.4 ,

View File

@@ -1,6 +1,6 @@
{
"material": "maps/de_inferno.png",
"material": "assets/maps/overview/de_inferno.png",
"pos_x": -2087 ,
"pos_y" : 3870,
"scale": 4.9,

View File

@@ -1,5 +1,5 @@
{
"material": "maps/de_mirage.png",
"material": "assets/maps/overview/de_mirage.png",
"pos_x": -3230,
"pos_y": 1713,
"scale": 5.00,

View File

@@ -0,0 +1,8 @@
{
"material": "assets/maps/overview/de_overpass.png",
"pos_x": -4831,
"pos_y": 1781,
"scale": 5.2
}

View File

Before

Width:  |  Height:  |  Size: 195 KiB

After

Width:  |  Height:  |  Size: 195 KiB

View File

Before

Width:  |  Height:  |  Size: 88 KiB

After

Width:  |  Height:  |  Size: 88 KiB

View File

Before

Width:  |  Height:  |  Size: 126 KiB

After

Width:  |  Height:  |  Size: 126 KiB

View File

Before

Width:  |  Height:  |  Size: 172 KiB

After

Width:  |  Height:  |  Size: 172 KiB

View File

Before

Width:  |  Height:  |  Size: 129 KiB

After

Width:  |  Height:  |  Size: 129 KiB

View File

Before

Width:  |  Height:  |  Size: 37 KiB

After

Width:  |  Height:  |  Size: 37 KiB

View File

Before

Width:  |  Height:  |  Size: 200 KiB

After

Width:  |  Height:  |  Size: 200 KiB

View File

@@ -1,5 +1,4 @@
import pygame
import copy
class ControlController:
def __init__(self, control_renderer, box_top: tuple[int, int]):

View File

@@ -22,13 +22,15 @@ def main():
},
"styling": {
"font": pygame.font.Font("assets/fonts/Metropolis-Regular.ttf", 30),
"small_font": pygame.font.Font("assets/fonts/Metropolis-Regular.ttf", 15),
"small_font": pygame.font.Font("assets/fonts/Metropolis-Regular.ttf", 18),
"underline_bold_font": (lambda f: (f.set_underline(True), f)[1])(pygame.font.Font("assets/fonts/Metropolis-Bold.ttf", 30)),
"button_colour": (200, 200, 200),
"pressed_button_colour": (150, 150, 150),
"text_colour": (255, 255, 255),
"background_colour": (30, 30, 30),
"foreground_colour": (100, 100, 100),
"player_selected_colour": (255, 255, 0),
"bomb_image": pygame.image.load("assets/images/bomb.png"),
}
}

View File

@@ -1,8 +0,0 @@
{
"material": "maps/de_ancient.png",
"pos_x": -2953,
"pos_y": 2164,
"scale": 5
}

View File

@@ -1,8 +0,0 @@
{
"material": "maps/de_overpass.png",
"pos_x": -4831,
"pos_y": 1781,
"scale": 5.2
}

View File

@@ -2,11 +2,13 @@ from models.player import Player
from models.team import Team
class Match:
def __init__(self, map_name, game_info, team_1: Team, team_2: Team, tick_rate=64):
def __init__(self, map_name, game_info, team_1: Team, team_2: Team, game_events, tick_rate=64):
self.team_1 = team_1
self.team_2 = team_2
self.map_name = map_name
self.game_events = game_events
self.round_start_times = list(self.game_events[0][1]["tick"])
self.tick = 1
self.current_tick = game_info[game_info["tick"] == self.tick]
@@ -17,28 +19,46 @@ class Match:
self.game_info = game_info.sort_values(by=["tick", "player_steamid"]) # pd dataframe sorted by tick
self.tick_rate = tick_rate
def _update_player_positions(self) -> None:
# inefficient, might need to change
def _update_player(self, player: Player) -> None:
player_row = self.current_tick[self.current_tick["player_steamid"] == player.steam_id]
if player_row.empty:
return
row = player_row.iloc[0]
player.x = row["X"]
player.y = row["Y"]
player.z = row["Z"]
player.pitch = row["pitch"]
player.yaw = row["yaw"]
player.dead = row["is_alive"] == 0
player.is_shooting = row["shots_fired"]
player.health = int(row["health"])
player.armour = int(row["armor_value"])
player.current_weapon = row["active_weapon_name"]
player.kills = int(row["kills_total"])
player.deaths = int(row["deaths_total"])
player.assists = int(row["assists_total"])
player.inventory = row["inventory"]
if "C4 Explosive" in player.inventory:
player.has_bomb = True
else:
player.has_bomb = False
if "Defuse Kit" in player.inventory:
player.has_defuser = True
else:
player.has_defuser = False
def _update_players(self) -> None:
# empty tick
if self.current_tick.empty:
return
# # check if current tick has NaN values
# if self.current_tick.isnull().values.any():
# return
for player in self.get_players():
player.x = self.current_tick[self.current_tick["player_steamid"] == player.steam_id]["X"].values[0]
player.y = self.current_tick[self.current_tick["player_steamid"] == player.steam_id]["Y"].values[0]
player.z = self.current_tick[self.current_tick["player_steamid"] == player.steam_id]["Z"].values[0]
player.pitch = self.current_tick[self.current_tick["player_steamid"] == player.steam_id]["pitch"].values[0]
player.yaw = self.current_tick[self.current_tick["player_steamid"] == player.steam_id]["yaw"].values[0]
player.dead = self.current_tick[self.current_tick["player_steamid"] == player.steam_id]["is_alive"].values[0] == 0
player.is_shooting = self.current_tick[self.current_tick["player_steamid"] == player.steam_id]["shots_fired"].values[0]
player.health = int(self.current_tick[self.current_tick["player_steamid"] == player.steam_id]["health"].values[0])
player.armour = int(self.current_tick[self.current_tick["player_steamid"] == player.steam_id]["armor_value"].values[0])
player.current_weapon = self.current_tick[self.current_tick["player_steamid"] == player.steam_id]["active_weapon_name"].values[0]
self._update_player(player)
def _update_round(self) -> None:
if self.current_tick.empty:
@@ -46,27 +66,34 @@ class Match:
self.round = self.current_tick["total_rounds_played"].values[0]
if self.round >= 12:
self.team_1.set_t()
self.team_2.set_ct()
self.team_1.score = int(self.current_tick[self.current_tick["team_num"] == 3]["team_rounds_total"].values[0])
self.team_2.score = int(self.current_tick[self.current_tick["team_num"] == 2]["team_rounds_total"].values[0])
else:
self.team_1.set_ct()
self.team_2.set_t()
self.team_1.score = int(self.current_tick[self.current_tick["team_num"] == 2]["team_rounds_total"].values[0])
self.team_2.score = int(self.current_tick[self.current_tick["team_num"] == 3]["team_rounds_total"].values[0])
self.team_1.score = int(self.current_tick[self.current_tick["team_num"] == self.team_1.id]["team_rounds_total"].values[0])
self.team_2.score = int(self.current_tick[self.current_tick["team_num"] == self.team_2.id]["team_rounds_total"].values[0])
def _update_team_ids(self) -> None:
# get random player from each team
if self.current_tick.empty:
return
for team in self.get_teams():
random_player = team.players[0]
player_team_id = self.current_tick[self.current_tick["player_steamid"] == random_player.steam_id]["team_num"].values[0]
if player_team_id == 2:
team.set_t()
elif player_team_id == 3:
team.set_ct()
def next_tick(self) -> None:
self.tick += 1
self.current_tick = self.game_info[self.game_info["tick"] == self.tick]
self._update_player_positions()
self._update_players()
self._update_team_ids()
self._update_round()
def set_tick(self, tick: int) -> None:
self.tick = tick
self.current_tick = self.game_info[self.game_info["tick"] == self.tick]
self._update_player_positions()
self._update_players()
self._update_round()
def get_players(self) -> list[Player]:

View File

@@ -13,6 +13,15 @@ class Player:
self.dead = False
self.is_shooting = False
self.current_weapon = None
self.has_bomb = False
self.is_planting = False
self.has_defuser = False
self.is_defusing = False
self.inventory = []
self.kills = 0
self.deaths = 0
self.assists = 0
## UI-related state
self.is_selected = False

View File

@@ -7,7 +7,8 @@ class Team:
def __init__(self):
self.players = []
self.score = 0
self.set_t()
self.id = 1
self.set_ct()
def add_player(self, player: Player):
self.players.append(player)
@@ -16,9 +17,9 @@ class Team:
self.players.remove(player)
def set_ct(self):
self.is_ct = True
self.id = 3
self.colour = CT_COLOUR
def set_t(self):
self.is_ct = False
self.id = 2
self.colour = T_COLOUR

View File

@@ -1,5 +1,5 @@
import pygame
from widgets.slider import HorizontalSlider
from widgets.break_slider import BreakSlider
class ControlRenderer:
def __init__(self, screen, match):
@@ -8,7 +8,10 @@ class ControlRenderer:
self.match = match
self.colour = (255, 255, 255)
self.slider = HorizontalSlider(self.screen, 50, 0, self.screen.get_width()-50, 20, 1, self.match.max_tick)
self.slider = BreakSlider(self.screen, 50, 0, self.screen.get_width()-50, 20, 1, self.match.max_tick)
self.slider.fill = True
self.slider.set_breakpoints(self.match.round_start_times)
self.slider.set_fill_colour((0,0,0))
def _render_slider(self):
# Update slider value

View File

@@ -10,10 +10,10 @@ class GUIRenderer:
# Buttons
self.settings_button = Button(self.screen.get_width()-40, 10, 30, 30, None)
self.settings_button.set_image("assets/setting.png")
self.settings_button.set_image("assets/images/setting.png")
self.back_button = Button(self.screen.get_width()-80, 10, 30, 30, None)
self.back_button.set_image("assets/arrow.png")
self.back_button.set_image("assets/images/arrow.png")
self.colour = (255, 255, 255)

View File

@@ -1,5 +1,3 @@
import pygame
class InfoRenderer:
def __init__(self, screen, styling, match):
self.screen = screen
@@ -8,25 +6,45 @@ class InfoRenderer:
self.colour = self.styling["text_colour"]
self.font = self.styling["font"]
self.small_font = self.styling["small_font"]
self.underline_bold_font = self.styling["underline_bold_font"]
self.selected_player = None
self.player_info_start_y = 100
self.match_info_start_y = 500
# Private methods
def _render_player_info(self):
"""Draws the player info on the screen."""
if self.selected_player is None:
return
player_info_title = f"{self.selected_player.name}\n"
player_info_title = "No player selected\n"
player_info = ""
else:
player_info_title = f"Player: {self.selected_player.name}\n"
player_info = f"Active Weapon: {self.selected_player.current_weapon}\n"
player_info += f"Inventory: \n - {"\n - ".join(self.selected_player.inventory)}\n"
player_info += f"Health: {self.selected_player.health}\n"
player_info += f"Armour: {self.selected_player.armour}\n"
player_info += f"Kills: {self.selected_player.kills}\n"
player_info += f"Deaths: {self.selected_player.deaths}\n"
player_info += f"Assists: {self.selected_player.assists}\n"
text_surface = self.font.render(player_info_title, True, self.styling["text_colour"])
self.screen.blit(text_surface, (10, 100))
text_surface = self.underline_bold_font.render(player_info_title, True, self.styling["text_colour"])
self.screen.blit(text_surface, (10, self.player_info_start_y))
text_surface = self.small_font.render(player_info, True, self.styling["text_colour"])
self.screen.blit(text_surface, (10, 150))
self.screen.blit(text_surface, (10, self.player_info_start_y + 50))
def _render_match_info(self):
"""Draws the match info on the screen."""
match_info_title = "Match Info\n"
match_info = f"Map: {self.match.map_name}\n"
match_info += f"Score: {self.match.team_1.score}-{self.match.team_2.score}\n"
text_surface = self.underline_bold_font.render(match_info_title, True, self.styling["text_colour"])
self.screen.blit(text_surface, (10, self.match_info_start_y))
text_surface = self.small_font.render(match_info, True, self.styling["text_colour"])
self.screen.blit(text_surface, (10, self.match_info_start_y + 50))
def _render_current_tick(self, match_tick, max_tick):
text = self.font.render(f"Tick: {match_tick}/{max_tick}", True, self.colour)
@@ -41,4 +59,4 @@ class InfoRenderer:
"""Renders the info on the screen."""
self._render_player_info()
self._render_current_tick(self.match.tick, self.match.max_tick)
self._render_team_scores(self.match.team_1.score, self.match.team_2.score)
self._render_match_info()

View File

@@ -13,9 +13,14 @@ class PlayerRenderer:
self.styling = styling
self.player_font = pygame.font.Font(None, 15)
self.bomb_image = self.styling["bomb_image"]
self.player_radius = 5
self.hovered_radius = 10
self.bomb_radius = 10
self.bomb_hovered_radius = 15
self.health_bar_foreground = (0, 255, 0)
self.health_bar_background = (255, 0, 0)
@@ -41,6 +46,10 @@ class PlayerRenderer:
## Private Methods
def _render_circle(self, player: Player, team):
if player.has_bomb:
self._render_bomb(player)
return
if player.is_hovered:
radius = self.hovered_radius
else:
@@ -49,6 +58,16 @@ class PlayerRenderer:
x, y = self.map_coord_converter.map_to_screen(player.x, player.y)
pygame.draw.circle(self.screen, team.colour, (x, y), radius)
def _render_bomb(self, player: Player):
if player.is_hovered:
radius = self.bomb_hovered_radius
else:
radius = self.bomb_radius
x, y = self.map_coord_converter.map_to_screen(player.x, player.y)
bomb_image = pygame.transform.scale(self.bomb_image, (radius*2, radius*2))
self.screen.blit(bomb_image, (x-radius, y-radius))
def _render_text(self, player):
if player.is_selected:
text = self.player_font.render(player.name, True, self.styling["player_selected_colour"])

View File

@@ -1,4 +1,3 @@
import pygame
from widgets.button import Button
from widgets.switch import Switch
@@ -21,7 +20,7 @@ class SettingsMenuRenderer:
self.back_button.set_colour(self.styling["button_colour"])
self.back_button.set_font(self.styling["font"])
self.back_button.set_pressed_colour(self.styling["pressed_button_colour"])
self.back_button.set_image("assets/arrow.png")
self.back_button.set_image("assets/images/arrow.png")
# Switches
self.show_yaw_button = Switch(self.widget_start_x, 100, 100, self.show_yaw_text.get_rect().height, self.options["show_yaw"])

View File

@@ -1,15 +1,13 @@
demoparser2==0.38.0
numpy==2.2.4
numpy==2.2.5
pandas==2.2.3
polars==1.25.2
pyarrow==19.0.1
pygame==2.6.1
polars==1.29.0
pyarrow==20.0.0
pygame-ce==2.5.3
pygame_gui==0.6.13
python-dateutil==2.9.0.post0
pytz==2025.1
python-i18n==0.3.9
pytz==2025.2
six==1.17.0
tqdm==4.67.1
tzdata==2025.1
pygame_gui==0.6.13
pygame-ce==2.5.3
pygame-pgu==0.21
python-i18n==0.3.9
tzdata==2025.2

View File

@@ -1,3 +1,4 @@
import time
from states.game_state import GameState
from controllers.player_controller import PlayerController
from render.map_renderer import MapRenderer
@@ -15,10 +16,19 @@ class Game(GameState):
def __init__(self, switch_state_callback, context):
super().__init__(switch_state_callback, context)
match_data_path = f"maps/{self.match.map_name}.json"
match_image_path = f"maps/{self.match.map_name}.png"
self.match_data_path = f"assets/maps/config/{self.match.map_name}.json"
self.match_image_path = f"assets/maps/overview/{self.match.map_name}.png"
# Screen Areas
self.game_update_interval = 1 / self.match.tick_rate
self.elapsed_time = 0
self.last_time = time.perf_counter()
self.__init_screen_areas()
self.__init_utils()
self.__init_renderers()
self.__init_controllers()
def __init_screen_areas(self):
self.info_box = pygame.Surface((350, self.screen.get_height()), pygame.SRCALPHA)
self.info_box_top_left = (0, 0)
@@ -28,17 +38,17 @@ class Game(GameState):
self.control_box = pygame.Surface((650, 120), pygame.SRCALPHA)
self.control_box_top_left = (350, 650)
# Helper Classes
self.map_coord_controller = MapCoordConverter(self.game_box.get_width(), self.game_box.get_height(), match_data_path, match_image_path)
def __init_utils(self):
self.map_coord_controller = MapCoordConverter(self.game_box.get_width(), self.game_box.get_height(), self.match_data_path, self.match_image_path)
# Renderers
self.map_renderer = MapRenderer(self.game_box, match_data_path, match_image_path)
def __init_renderers(self):
self.map_renderer = MapRenderer(self.game_box, self.match_data_path, self.match_image_path)
self.player_renderer = PlayerRenderer(self.game_box, self.match, self.map_coord_controller, self.options, self.styling)
self.gui_render = GUIRenderer(self.screen, self.match)
self.info_render = InfoRenderer(self.info_box, self.styling, self.match)
self.control_render = ControlRenderer(self.control_box, self.match)
# Controllers
def __init_controllers(self):
self.player_controller = PlayerController(self.player_renderer, self.match, self.game_box_top_left)
self.gui_controller = GUIController(self.gui_render, self.switch_state)
self.info_controller = InfoController(self.info_render, self.player_controller)
@@ -55,8 +65,15 @@ class Game(GameState):
self.control_controller.update(event)
def update(self):
"""Updates game objects."""
"""Fixed-timestep update decoupled from frame rate."""
now = time.perf_counter()
delta = now - self.last_time
self.last_time = now
self.elapsed_time += delta
while self.elapsed_time >= self.game_update_interval:
self.match.next_tick()
self.elapsed_time -= self.game_update_interval
def draw(self):
"""Draws everything on screen."""

View File

@@ -1,9 +1,6 @@
from states.game_state import GameState
from widgets.button import Button
from widgets.switch import Switch
from controllers.settings_controller import SettingsController
from render.settings_menu_renderer import SettingsMenuRenderer
import pygame
class SettingsMenu(GameState):
def __init__(self, switch_state_callback, context):

View File

@@ -3,7 +3,6 @@ from widgets.button import Button
from models.match import Match
from models.player import Player
from models.team import Team
from pgu import gui
import demoparser2
import pygame
import pygame_gui
@@ -17,7 +16,7 @@ class StartMenu(GameState):
self.default_button_width = self.screen.get_width() * 0.8
# logo
self.logo = pygame.image.load("assets/logo.png").convert_alpha()
self.logo = pygame.image.load("assets/images/logo.png").convert_alpha()
self.logo_scale = 0.7
self.logo = pygame.transform.smoothscale(self.logo, (self.logo.get_rect().size[0] * self.logo_scale, self.logo.get_rect().size[1] * self.logo_scale))
@@ -84,15 +83,19 @@ class StartMenu(GameState):
demo_parser = demoparser2.DemoParser(demo_file)
game_info = demo_parser.parse_ticks(["X", "Y", "Z", "pitch", "yaw", "is_alive", "team", "player_steamid",
"team_rounds_total", "team_num", "total_rounds_played", "shots_fired",
"kills_total", "deaths_total", "assists_total", "inventory",
"health", "armor_value", "active_weapon_name"])
round_changes = demo_parser.parse_events(["round_start", "is_ct_timeout"])
header_info = demo_parser.parse_header()
map_name = header_info['map_name']
players = demo_parser.parse_player_info()
print(demo_parser.list_game_events())
team_1 = Team()
team_1.set_ct()
team_2 = Team()
m = Match(map_name, game_info, team_1, team_2, self.options)
team_2.set_ct()
m = Match(map_name, game_info, team_1, team_2, round_changes)
for index, row in players.iterrows():
if row["team_number"] == 2:
team_1.add_player(Player(row["name"], row["steamid"]))

55
widgets/break_slider.py Normal file
View File

@@ -0,0 +1,55 @@
from widgets.slider import HorizontalSlider
import pygame
class BreakSlider(HorizontalSlider):
def __init__(self, screen, x, y, width, height, min_value, max_value):
super().__init__(screen, x, y, width, height, min_value, max_value)
self.breakpoints = []
# defaults
self.breakpoint_colour = (0, 0, 0)
self.breakpoint_line_width = 2
def add_breakpoint(self, breakpoint):
if breakpoint < self.min_value or breakpoint > self.max_value:
raise ValueError("Breakpoint must fit between min and max values")
self.breakpoints.append(breakpoint)
def set_breakpoints(self, breakpoints):
self.breakpoints = breakpoints
def _draw_breakpoints(self):
for breakpoint in self.breakpoints:
# create line
break_x = self._value_to_knob(breakpoint)
pygame.draw.line(self.screen,
self.breakpoint_colour,
(break_x, self.y),
(break_x, self.y + self.height),
width=self.breakpoint_line_width)
def draw(self):
self._draw_slider()
self._draw_breakpoints()
self._draw_knob()
if __name__ == "__main__":
pygame.init()
screen = pygame.display.set_mode((800, 600))
slider = BreakSlider(screen, 50, 50, 700, 20, 0, 1000)
slider.add_breakpoint(100)
slider.add_breakpoint(500)
slider.add_breakpoint(700)
running = True
while running:
screen.fill((0, 0, 0))
slider.draw()
pygame.display.flip()
for event in pygame.event.get():
if event.type == pygame.QUIT:
running = False
slider.handle_event(event)

View File

@@ -41,7 +41,7 @@ class Button:
def set_font_size(self, font_size: int) -> None:
self.font_size = font_size
def set_font(self, font: pygame.Font) -> None:
def set_font(self, font: pygame.font.Font) -> None:
self.font = font
def set_text(self, text: str) -> None:

View File

@@ -15,7 +15,7 @@ class HorizontalSlider:
self.knob_colour = (255, 0, 0)
self.rect_radius = 5
self.knob_radius = 10
self.knob_radius = 5
self.knob_x = self.x + self.knob_radius
self.dragging = False
@@ -54,16 +54,23 @@ class HorizontalSlider:
self.knob_x = event.pos[0]
self.value = self._knob_to_value(self.knob_x)
def draw(self):
"""
Draw the slider on the screen
"""
def _draw_slider(self):
if self.fill:
pygame.draw.rect(self.screen, self.fill_colour, (self.x, self.y, self.knob_x, self.height), border_radius=self.rect_radius)
pygame.draw.rect(self.screen, self.background_colour, (self.knob_x, self.y, self.width - (self.knob_x - self.x), self.height), border_radius=self.rect_radius)
else:
pygame.draw.rect(self.screen, self.background_colour, (self.x, self.y, self.width, self.height), border_radius=self.rect_radius)
pygame.draw.circle(self.screen, self.knob_colour, (int(self.knob_x), self.y + self.height // 2), self.knob_radius)
def _draw_knob(self):
pygame.draw.line(self.screen, self.knob_colour, (self.knob_x, self.y), (self.knob_x, self.y + self.height), width=self.knob_radius)
def draw(self):
"""
Draw the slider on the screen
"""
self._draw_slider()
self._draw_knob()
def set_value(self, value):
"""
@@ -89,6 +96,9 @@ class HorizontalSlider:
def set_knob_colour(self, colour):
self.knob_colour = colour
def set_fill_colour(self, colour):
self.fill_colour = colour
if __name__ == "__main__":
pygame.init()
screen = pygame.display.set_mode((800, 600))