| import random |
| import enum |
| from functools import total_ordering |
|
|
| """ |
| An exact implementation of http://scoresim.ilya.online |
| """ |
|
|
|
|
| @total_ordering |
| class OrderedEnum(enum.Enum): |
| def __ge__(self, other): |
| if self.__class__ is other.__class__: |
| return self.value >= other.value |
| return NotImplemented |
|
|
|
|
| def random_picker(weights): |
| total_weight = sum(weights.values()) |
| random_value = random.uniform(0, total_weight) |
|
|
| cumulative_weight = 0 |
| for item, weight in weights.items(): |
| cumulative_weight += weight |
| if random_value <= cumulative_weight: |
| return item |
|
|
|
|
| class PlayerPosition(OrderedEnum): |
| GOALKEEPER = 1 |
| DEFENDER = 2 |
| MIDFIELDER = 3 |
| FORWARD = 4 |
|
|
|
|
| class CardColour(OrderedEnum): |
| YELLOW = 1 |
| RED = 2 |
|
|
|
|
| class Half(OrderedEnum): |
| FIRST_HALF = 1 |
| SECOND_HALF = 2 |
| EXTRA_TIME = 3 |
| PENALTIES = 4 |
| MATCH_END = 5 |
|
|
| @total_ordering |
| class Time: |
| __slots__ = ("half", "minute", "time_added_h1", "time_added_h2") |
|
|
| def __init__(self, half: Half, minute: int): |
| self.minute = minute |
| self.time_added_h1 = None |
| self.time_added_h2 = None |
| self.half = half |
|
|
| def __str__(self): |
| if self.half == Half.FIRST_HALF: |
| if self.minute > 45: |
| return f"45+{self.minute - 45}'" |
| else: |
| return str(self.minute) + "'" |
| elif self.half == Half.SECOND_HALF: |
| if self.minute > 90: |
| return f"90+{self.minute - 90}'" |
| else: |
| return str(self.minute) + "'" |
| elif self.half == Half.EXTRA_TIME: |
| if self.minute > 120: |
| return f"120+{self.minute - 120}'" |
| else: |
| return str(self.minute) + "'" |
| elif self.half == Half.MATCH_END: |
| return str(self.minute) + "'" |
|
|
| def __eq__(self, other): |
| return self.half == other.half and self.minute == other.minute |
|
|
| def __gt__(self, other): |
| if not self.half == other.half: |
| return self.half > other.half |
| else: |
| return self.minute > other.minute |
|
|
|
|
| class Shot: |
| __slots__ = ("player", "time", "on_target", "goal", "team", "penalty", "assister", "blocker") |
|
|
| def __init__(self, player: str, time: Time, on_target: bool, goal: bool, team: str, penalty: bool = None, assister: str = None, blocker: str = None): |
| self.player = player |
| self.time = time |
| self.on_target = on_target |
| self.goal = goal |
| self.team = team |
| self.penalty = False if penalty is None else penalty |
| self.assister = assister |
| self.blocker = blocker |
|
|
|
|
| class Card: |
| __slots__ = ("player", "colour", "time", "team") |
|
|
| def __init__(self, player, colour: CardColour, time: Time, team: str): |
| self.player = player |
| self.colour = colour |
| self.time = time |
| self.team = team |
|
|
|
|
| class Player: |
| __slots__ = ("name", "goals", "yellow_card", "red_card", "position") |
|
|
| def __init__(self, name: str, position: PlayerPosition): |
| self.name = name |
| self.goals = 0 |
| self.yellow_card = None |
| self.red_card = None |
| self.position = position |
|
|
|
|
| class TeamIdentifier(enum.Enum): |
| TEAM_1 = 1 |
| TEAM_2 = 2 |
|
|
|
|
| class Team: |
| __slots__ = ("name", "players", "goals", "relative_strength", "shot_accuracy", "possession", "potential_fouls", |
| "potential_shots", "tactic", "morale", "crowd_support", "fouls", "sent_off_players", "shots", |
| "shots_on_target", "goal_objects", "cards", "shootout_goals", "penalty_objects", "first_leg_goals") |
|
|
| TACTICS = {"Very defensive": 0.6, "Defensive": 0.8, "Balanced": 1, "Attacking (recommended)": 1.2, |
| "Very attacking": 1.4} |
| MORALES = {"Extremely low": 0, "Very low": 1, "Low": 2, "Average": 3, "High": 4, "Very high": 5, "Extremely high": 6} |
| CROWD_SUPPORTS = {"No support": 0, "Weak support": 2, "Average support": 3, "Good support": 4, "Great Support": 6, |
| "Outstanding support": 8, "Away team (no support)": 0} |
| STRENGTHS = { |
| "Team 1 much weaker than Team 2": 30, |
| "Team 1 weaker than Team 2": 40, |
| "Team 1 a bit weaker than Team 2": 45, |
| "Team 1 and Team 2 equal in class": 50, |
| "Team 1 a bit stronger than Team 2": 55, |
| "Team 1 stronger than Team 2": 60, |
| "Team 1 much stronger than Team 2": 70, |
| } |
|
|
| def __init__(self, name: str, players: list[Player], goals: int = None, relative_strength: int = None, |
| tactic: str = None, |
| opponent_tactic: str = None, morale: str = None, crowd_support=None): |
| self.name = name |
| self.players = players |
| self.first_leg_goals = goals |
| self.goals = 0 if goals is None else goals |
| self.shootout_goals = 0 |
| self.relative_strength = 50 if relative_strength is None else relative_strength |
| self.relative_strength += round((random.random() - self.relative_strength / 100) * 10) |
| |
| self.shot_accuracy = random.random() * 0.25 + 0.3 |
| self.possession = 50 |
| self.potential_fouls = round(round(20 + random.random() * 7) * relative_strength / 100) |
| tactic = "Balanced" if tactic is None else tactic |
| self.tactic = self.TACTICS[tactic] |
| opponent_tactic = "Balanced" if opponent_tactic is None else opponent_tactic |
| opponent_tactic = self.TACTICS[opponent_tactic] |
| if self.tactic > opponent_tactic: |
| self.potential_shots = 8 * (self.tactic + opponent_tactic) |
| elif self.tactic == opponent_tactic: |
| if self.tactic >= 1: |
| self.potential_shots = 12 * (self.tactic + opponent_tactic) |
| if self.tactic < 1: |
| self.potential_shots = 8 * (self.tactic + opponent_tactic) |
| elif self.tactic < opponent_tactic: |
| self.potential_shots = 6 * (self.tactic + opponent_tactic) |
| print(self.potential_shots) |
| morale = "Average" if morale is None else morale |
| self.morale = self.MORALES[morale] |
| crowd_support = "No support" if crowd_support is None else crowd_support |
| self.crowd_support = self.CROWD_SUPPORTS[crowd_support] |
| self.relative_strength += self.crowd_support |
| if self.relative_strength < 60 and opponent_tactic < 1: |
| self.potential_shots *= opponent_tactic * max(self.relative_strength, 25) / 50 |
| self.potential_shots = round( |
| self.potential_shots * ((self.relative_strength + self.morale + self.crowd_support) * 1.2 - 20) / 90) + 3 |
| print(self.potential_shots) |
| self.fouls = 0 |
| self.shots = 0 |
| self.shots_on_target = 0 |
| self.sent_off_players = [] |
| self.goal_objects = [] |
| self.penalty_objects = [] |
| self.cards = [] |
|
|
|
|
| def possession_change(team_1: Team, team_2: Team): |
| possession_adjust = 0 |
| if team_1.relative_strength < team_2.relative_strength: |
| if team_1.relative_strength <= 40: |
| possession_adjust = random.random() * 5 |
| elif team_1.relative_strength >= 45: |
| possession_adjust = random.random() * 3 |
| elif team_1.relative_strength > team_2.relative_strength: |
| if team_2.relative_strength <= 40: |
| possession_adjust = random.random() * -5 |
| elif team_2.relative_strength >= 45: |
| |
| |
| possession_adjust = random.random() * -3 |
| morale_from_possession = random.random() * 0.02 |
| if random.random() < (team_1.relative_strength + possession_adjust) / 100: |
| if abs(team_1.possession - team_2.possession) > 20: |
| team_1.possession = team_1.possession + round(random.random() * 0.75) |
| else: |
| team_1.possession = team_1.possession + round(random.random() * 1.4) |
| team_1.morale += morale_from_possession |
| team_2.morale -= morale_from_possession |
| team_2.possession = 100 - team_1.possession |
| else: |
| if abs(team_1.possession - team_2.possession) > 20: |
| team_2.possession = team_2.possession + round(random.random() * 0.75) |
| else: |
| team_2.possession = team_2.possession + round(random.random() * 1.4) |
| team_2.morale += morale_from_possession |
| team_1.morale -= morale_from_possession |
| team_1.possession = 100 - team_2.possession |
| |
| if team_1.possession - team_2.possession > 70: |
| team_1.possession = 85 |
| team_2.possession = 15 |
| elif team_2.possession - team_1.possession > 70: |
| team_2.possession = 85 |
| team_1.possession = 15 |
| return team_1, team_2 |
|
|
|
|
| def foul_change(team_1: Team, team_2: Team, half: Half, minute: int, minute_events: list): |
| factor = 2 if half == Half.EXTRA_TIME else 1 |
| team_1_foul = team_1.potential_fouls / 90 * factor - random.random() |
| team_2_foul = team_2.potential_fouls / 90 * factor - random.random() |
| if team_1_foul > 0 and team_1_foul > team_2_foul: |
| team_1.fouls += 1 |
| |
| if random.random() < 1 / 7: |
| bad_player = random.randint(0, len(team_1.players) - 1) |
| if team_1.players[bad_player].yellow_card: |
| if random.random() < 0.66: |
| bad_player = random.randint(0, len(team_1.players) - 1) |
| yellow_card = Card(team_1.players[bad_player].name, CardColour.YELLOW, Time(half, minute), team_1.name) |
| red_card = Card(team_1.players[bad_player].name, CardColour.RED, Time(half, minute), team_1.name) |
| |
| print(f"{team_1.players[bad_player].name} {str(team_1.players[bad_player].yellow_card)} {minute}") |
| if team_1.players[bad_player].yellow_card: |
| team_1.cards.append(yellow_card) |
| team_1.cards.append(red_card) |
| minute_events.append(yellow_card) |
| minute_events.append(red_card) |
| team_1.sent_off_players.append(team_1.players[bad_player]) |
| del team_1.players[bad_player] |
| |
| team_1.relative_strength *= 0.91 |
| |
| if random.random() > 0.5: |
| team_1.morale *= 0.85 |
| else: |
| team_1.morale *= 1.15 |
| else: |
| team_1.players[bad_player].yellow_card = yellow_card |
| team_1.cards.append(yellow_card) |
| minute_events.append(yellow_card) |
| |
| elif random.random() < 1 / 500: |
| bad_player = random.randint(0, len(team_1.players) - 1) |
| red_card = Card(team_1.players[bad_player].name, CardColour.RED, Time(half, minute), team_1.name) |
| team_1.cards.append(red_card) |
| minute_events.append(red_card) |
| team_1.sent_off_players.append(team_1.players[bad_player]) |
| del team_1.players[bad_player] |
| |
| team_1.relative_strength *= 0.91 |
| |
| if random.random() > 0.5: |
| team_1.morale *= 0.8 |
| else: |
| team_1.morale *= 1.2 |
| |
| if team_2_foul > 0 and team_2_foul > team_1_foul: |
| team_2.fouls += 1 |
| |
| if random.random() < 1 / 7: |
| bad_player = random.randint(0, len(team_2.players) - 1) |
| if team_2.players[bad_player].yellow_card: |
| if random.random() < 0.66: |
| bad_player = random.randint(0, len(team_2.players) - 1) |
| |
| yellow_card = Card(team_2.players[bad_player].name, CardColour.YELLOW, Time(half, minute), team_2.name) |
| red_card = Card(team_2.players[bad_player].name, CardColour.RED, Time(half, minute), team_2.name) |
| print(f"{team_2.players[bad_player].name} {str(team_2.players[bad_player].yellow_card)} {minute}") |
| if team_2.players[bad_player].yellow_card: |
| team_2.cards.append(yellow_card) |
| team_2.cards.append(red_card) |
| minute_events.append(yellow_card) |
| minute_events.append(red_card) |
| team_2.sent_off_players.append(team_2.players[bad_player]) |
| del team_2.players[bad_player] |
| |
| team_2.relative_strength *= 0.91 |
| |
| if random.random() > 0.5: |
| team_2.morale *= 0.85 |
| else: |
| team_2.morale *= 1.15 |
| else: |
| team_2.players[bad_player].yellow_card = yellow_card |
| team_2.cards.append(yellow_card) |
| minute_events.append(yellow_card) |
| |
| elif random.random() < 1 / 500: |
| bad_player = random.randint(0, len(team_2.players) - 1) |
| red_card = Card(team_2.players[bad_player].name, CardColour.RED, Time(half, minute), team_2.name) |
| team_2.cards.append(red_card) |
| minute_events.append(red_card) |
| team_2.sent_off_players.append(team_2.players[bad_player]) |
| del team_2.players[bad_player] |
| |
| team_2.relative_strength *= 0.91 |
| |
| if random.random() > 0.5: |
| team_2.morale *= 0.8 |
| else: |
| team_2.morale *= 1.2 |
| return team_1, team_2, minute_events |
|
|
|
|
| def goal_scorer(team: Team): |
| weights = { |
| PlayerPosition.FORWARD: 0.6, |
| PlayerPosition.MIDFIELDER: 0.35, |
| PlayerPosition.DEFENDER: 0.05, |
| } |
| player_position = random_picker(weights) |
| valid_players = [index for index, scorer in enumerate(team.players) if scorer.position == player_position] |
| if valid_players: |
| player = random.choice(valid_players) |
| else: |
| player = random.randint(0, len(team.players) - 1) |
| return player |
|
|
|
|
| def assist_player(team: Team, scorer: int): |
| weights = { |
| PlayerPosition.FORWARD: 0.43, |
| PlayerPosition.MIDFIELDER: 0.35, |
| PlayerPosition.DEFENDER: 0.1, |
| PlayerPosition.GOALKEEPER: 0.02, |
| None: 0.1 |
| } |
| player_position = random_picker(weights) |
| valid_players = [index for index, scorer in enumerate(team.players) if scorer.position == player_position] |
| try: |
| valid_players.remove(scorer) |
| except ValueError: |
| pass |
| if valid_players: |
| player = random.choice(valid_players) |
| else: |
| player = random.randint(0, len(team.players) - 1) |
| if not scorer == player: |
| return player |
| else: |
| return |
|
|
|
|
| def block_player(team: Team): |
| weights = { |
| PlayerPosition.FORWARD: 0.05, |
| PlayerPosition.MIDFIELDER: 0.15, |
| PlayerPosition.DEFENDER: 0.80, |
| } |
| player_position = random_picker(weights) |
| valid_players = [index for index, scorer in enumerate(team.players) if scorer.position == player_position] |
| if valid_players: |
| player = random.choice(valid_players) |
| else: |
| player = random.randint(0, len(team.players) - 1) |
| return player |
|
|
|
|
| def goal_change(team_1: Team, team_2: Team, half: Half, minute: int, minute_events: list): |
| |
| factor = 2 if half == Half.EXTRA_TIME else 1 |
| team_1_shot = team_1.potential_shots / 90 * factor - random.random() |
| team_2_shot = team_2.potential_shots / 90 * factor - random.random() |
| if team_1_shot > 0 and team_1_shot > team_2_shot: |
| team_1.shots += 1 |
| player = goal_scorer(team_1) |
| assister = assist_player(team_1, player) |
| if assister is not None: |
| assister = team_1.players[assister].name |
| |
| if random.random() < team_1.shot_accuracy: |
| team_1.shots_on_target += 1 |
| team_1.morale += 0.2 |
| |
| goal_chance = (team_1.relative_strength + team_1.morale * 2 + team_1.crowd_support) / 100 / ( |
| 1.5 + random.random()) |
| if random.random() < goal_chance: |
| |
| shot = Shot(team_1.players[player].name, Time(half, minute), on_target=True, goal=True, |
| team=team_1.name, assister=assister) |
| team_1.goals += 1 |
| team_1.goal_objects.append(shot) |
| minute_events.append(shot) |
| team_1.players[player].goals += 1 |
| team_1.morale += 1 |
| else: |
| shot = Shot(team_1.players[player].name, Time(half, minute), on_target=True, goal=False, |
| team=team_1.name, assister=assister) |
| minute_events.append(shot) |
| else: |
| blocker = team_2.players[block_player(team_2)].name |
| shot = Shot(team_1.players[player].name, Time(half, minute), on_target=False, goal=False, |
| team=team_1.name, assister=assister, blocker=blocker) |
| minute_events.append(shot) |
| elif team_2_shot > 0 and team_2_shot > team_1_shot: |
| team_2.shots += 1 |
| player = goal_scorer(team_2) |
| assister = assist_player(team_2, player) |
| if assister is not None: |
| assister = team_2.players[assister].name |
| |
| if random.random() < team_2.shot_accuracy: |
| team_2.shots_on_target += 1 |
| team_2.morale += 0.2 |
| |
| goal_chance = (team_2.relative_strength + team_2.morale * 2 + team_2.crowd_support) / 100 / ( |
| 1.5 + random.random()) |
| if random.random() < goal_chance: |
| |
| shot = Shot(team_2.players[player].name, Time(half, minute), on_target=True, goal=True, |
| team=team_2.name, assister=assister) |
| team_2.goals += 1 |
| team_2.goal_objects.append(shot) |
| minute_events.append(shot) |
| team_2.players[player].goals += 1 |
| team_2.morale += 1 |
| else: |
| shot = Shot(team_2.players[player].name, Time(half, minute), on_target=True, goal=False, |
| team=team_2.name, assister=assister) |
| minute_events.append(shot) |
| else: |
| blocker = team_1.players[block_player(team_1)].name |
| shot = Shot(team_2.players[player].name, Time(half, minute), on_target=False, goal=False, |
| team=team_2.name, assister=assister, blocker=blocker) |
| minute_events.append(shot) |
| return team_1, team_2, minute_events |
|
|
|
|
| def new_minute(team_1: Team, team_2: Team, half: Half, minute: int, extra_time: bool = None): |
| """Simulates a minute of football""" |
| minute_events = [] |
| |
| team_1, team_2 = possession_change(team_1, team_2) |
| |
| team_1, team_2, minute_events = foul_change(team_1, team_2, half, minute, minute_events) |
| |
| team_1, team_2, minute_events = goal_change(team_1, team_2, half, minute, minute_events) |
| if minute == 45 and half == Half.FIRST_HALF: |
| half = Half.SECOND_HALF |
| if minute == 90 and half == Half.SECOND_HALF: |
| if extra_time is True and team_1.goals == team_2.goals: |
| half = Half.EXTRA_TIME |
| else: |
| half = Half.MATCH_END |
| if minute == 120 and half == Half.EXTRA_TIME: |
| half = Half.MATCH_END |
| return team_1, team_2, half, minute_events |
|
|
|
|
| def penalty_winner(team_1_goals, team_2_goals, penalty_turn, team_shooting): |
| penalty_turn += 1 |
| if penalty_turn <= 5: |
| if team_shooting == TeamIdentifier.TEAM_1: |
| team_1_chances = 5 - penalty_turn |
| team_2_chances = 5 - penalty_turn + 1 |
| if team_1_goals + team_1_chances < team_2_goals: |
| return TeamIdentifier.TEAM_2 |
| elif team_2_goals + team_2_chances < team_1_goals: |
| return TeamIdentifier.TEAM_1 |
| if team_shooting == TeamIdentifier.TEAM_2: |
| chances = 5 - penalty_turn |
| if team_2_goals + chances < team_1_goals: |
| return TeamIdentifier.TEAM_1 |
| elif team_1_goals + chances < team_2_goals: |
| return TeamIdentifier.TEAM_2 |
| else: |
| if team_shooting == TeamIdentifier.TEAM_2: |
| if team_2_goals < team_1_goals: |
| return TeamIdentifier.TEAM_1 |
| elif team_1_goals < team_2_goals: |
| return TeamIdentifier.TEAM_2 |
| return False |
|
|
|
|
| async def penalty_shootout(team_1: Team, team_2: Team): |
| team_1_players = team_1.players |
| team_2_players = team_2.players |
| team_1_players.sort(key=lambda player: player.position, reverse=True) |
| team_2_players.sort(key=lambda player: player.position, reverse=True) |
| if len(team_1_players) > len(team_2_players): |
| team_1_players = team_1_players[:len(team_2_players)-len(team_1_players)] |
| elif len(team_1_players) < len(team_2_players): |
| team_2_players = team_2_players[:len(team_1_players)-len(team_2_players)] |
| shootout_winner = False |
| team_shooting = TeamIdentifier.TEAM_1 |
| penalty_turn = 0 |
| while not shootout_winner: |
| player = penalty_turn % len(team_1_players) |
| penalty_scored = random.random() < 0.75 |
| if team_shooting == TeamIdentifier.TEAM_1: |
| penalty_shot = Shot( |
| team_1_players[player].name, |
| Time(Half.PENALTIES, penalty_turn), |
| True, |
| penalty_scored, |
| team_1.name, |
| penalty=True |
| ) |
| team_1.penalty_objects.append(penalty_shot) |
| if penalty_scored: |
| team_1.shootout_goals += 1 |
| elif team_shooting == TeamIdentifier.TEAM_2: |
| penalty_shot = Shot( |
| team_2_players[player].name, |
| Time(Half.PENALTIES, penalty_turn), |
| True, |
| penalty_scored, |
| team_2.name, |
| penalty=True |
| ) |
| team_2.penalty_objects.append(penalty_shot) |
| if penalty_scored: |
| team_2.shootout_goals += 1 |
| yield penalty_shot, team_1, team_2 |
| shootout_winner = penalty_winner(team_1.shootout_goals, team_2.shootout_goals, penalty_turn, team_shooting) |
| if not shootout_winner: |
| if team_shooting == TeamIdentifier.TEAM_1: |
| team_shooting = TeamIdentifier.TEAM_2 |
| elif team_shooting == TeamIdentifier.TEAM_2: |
| team_shooting = TeamIdentifier.TEAM_1 |
| penalty_turn += 1 |
|
|
|
|
| async def start_match(team_1: Team, team_2: Team, extra_time: bool = None, penalties: bool = None): |
| minute = 1 |
| half = Half.FIRST_HALF |
| extra_time = False if extra_time is None else extra_time |
| penalties = False if penalties is None else penalties |
| for player in team_1.players: |
| print(f"{player.name}, {player.yellow_card}") |
| for player in team_2.players: |
| print(f"{player.name}, {player.yellow_card}") |
| while not half == Half.MATCH_END: |
| team_1, team_2, half, minute_events = new_minute(team_1, team_2, half, minute, extra_time) |
| yield team_1, team_2, Time(half, minute), minute_events |
| minute += 1 |
|
|
|
|
| def default_players(): |
| return [ |
| Player("Goalkeeper", PlayerPosition.GOALKEEPER), |
| Player("Defender1", PlayerPosition.DEFENDER), |
| Player("Defender2", PlayerPosition.DEFENDER), |
| Player("Defender3", PlayerPosition.DEFENDER), |
| Player("Defender4", PlayerPosition.DEFENDER), |
| Player("Midfielder1", PlayerPosition.MIDFIELDER), |
| Player("Midfielder2", PlayerPosition.MIDFIELDER), |
| Player("Midfielder3", PlayerPosition.MIDFIELDER), |
| Player("Midfielder4", PlayerPosition.MIDFIELDER), |
| Player("Forward1", PlayerPosition.FORWARD), |
| Player("Forward2", PlayerPosition.FORWARD), |
| ] |
|
|