My program applies stacking penalty rule in Daily Fantasy Football by choosing which players to be penalized by which amount of points.

The rule is about penalizing teams with two or more defensive players when the team conceded no goals.

Here is the description of the rule:

For the 2019/20 football season, there is a new, important rule in place for daily fantasy tournaments: If you pick more than one defensive player (included keeper) from a club, the points awarded for clean sheet will decrease by 1 point for each additional defensive player from the same club. The stacking penalty has a maximum of minus 3 points.

1st defensive player: 0 points

2nd defensive player: -1 points

3rd defensive player: -2 points

4th-6th defensive player: -3 points

It is actually quite easy; if you have three defensive players from the same club, one will have no stacking penalty (he gets 4 points for clean sheet), one will get minus 1 (so he gets 3 points for a clean sheet), and one will get minus 2 (he gets 2 points for a clean sheet).

**How to choose players to be penalized:**

We rank the players that are on your team. When ranking defensive players from the same club, the priority is:

Captain > Vice Captain > Price > Last name.

For price, the higher price will rank highest. For last name, early position in the alphabet will rank highest.

So, if you have a defensive player as captain he will never get a stacking penalty. If you don’t have a defensive player from this club as captain, but instead as vice captain, he will not get a stacking penalty. If there is no captain or vice captain in defense, the highest priced player will be 1st defensive player, and won’t get a stacking penalty.

I would like to get feedback on my way of decomposing this problem and how to reduce highly repetitive functions that work with particular amount of defensive players to more general one.

Here is the code:

```
"""
Applying stacking penalty rule to a team in Fantasy Football.
"""
from dataclasses import dataclass
from typing import List, Tuple
@dataclass
class Player:
"""
Represents a player in Fantasy Football with his id, name, club,
playing position, pursuit points and whether he is the captain
or vice-captain.
"""
p_id: int
name: str
club: str
position: str
pursuit: float
is_captain: bool = False
is_vice_captain: bool = False
PlayerId = int
PenalizedPlayer = Tuple(PlayerId, int)
PenalizedPlayers = List(PenalizedPlayer)
Team = List(Player)
players = (
Player(88472, "Leno", "ARS", "goalkeeper", -0.88),
Player(160699, "Mari", "ARS", "defender", -0.52),
Player(139989, "Tierney", "ARS", "defender", -0.72),
Player(24075, "Holding", "ARS", "defender", -0.60),
Player(23332, "Bellerin", "ARS", "defender", -0.76),
Player(197966, "Smith Rowe", "ARS", "midfielder", -1.24),
Player(137793, "Ceballos", "ARS", "midfielder", -1.08),
Player(106430, "Saka", "ARS", "midfielder", -1.72),
Player(23337, "Xhaka", "ARS", "midfielder", -0.84),
Player(77663, "Aubameyang", "ARS", "forward", -1.88),
Player(45079, "Lacazette", "ARS", "forward", -2.00),
)
team1 = (
Player(88472, "Leno", "ARS", "goalkeeper", -0.88, is_captain=True),
Player(139989, "Tierney", "ARS", "defender", -0.72),
Player(24075, "Holding", "ARS", "defender", -0.60),
Player(77663, "Aubameyang", "ARS", "forward", -1.88),
Player(45079, "Lacazette", "ARS", "forward", -2.00, is_vice_captain=True)
)
team2 = (
Player(88472, "Leno", "ARS", "goalkeeper", -0.88),
Player(139989, "Tierney", "ARS", "defender", -0.72),
Player(24075, "Holding", "ARS", "defender", -0.60, is_captain=True),
Player(77663, "Aubameyang", "ARS", "forward", -1.88),
Player(45079, "Lacazette", "ARS", "forward", -2.00, is_vice_captain=True)
)
team3 = (
Player(88472, "Leno", "ARS", "goalkeeper", -0.88),
Player(139989, "Tierney", "ARS", "defender", -0.72, is_vice_captain=True),
Player(24075, "Holding", "ARS", "defender", -0.60, is_captain=True),
Player(77663, "Aubameyang", "ARS", "forward", -1.88),
Player(45079, "Lacazette", "ARS", "forward", -2.00)
)
team4 = (
Player(88472, "Leno", "ARS", "goalkeeper", -0.88),
Player(139989, "Tierney", "ARS", "defender", -0.72, is_captain=True),
Player(24075, "Holding", "ARS", "defender", -0.60),
Player(160699, "Mari", "ARS", "defender", -0.52),
Player(106430, "Saka", "ARS", "midfielder", -1.72, is_vice_captain=True)
)
team5 = (
Player(88472, "Leno", "ARS", "goalkeeper", -0.88, is_captain=True),
Player(160699, "Mari", "ARS", "defender", -0.52, is_vice_captain=True),
Player(139989, "Tierney", "ARS", "defender", -0.72),
Player(24075, "Holding", "ARS", "defender", -0.60),
Player(23332, "Bellerin", "ARS", "defender", -0.76)
)
team6 = (
Player(139989, "Tierney", "ARS", "defender", -0.72),
Player(137793, "Ceballos", "ARS", "midfielder", -1.08),
Player(106430, "Saka", "ARS", "midfielder", -1.72),
Player(23337, "Xhaka", "ARS", "midfielder", -0.84),
Player(77663, "Aubameyang", "ARS", "forward", -1.88, is_captain=True),
Player(45079, "Lacazette", "ARS", "forward", -2.00, is_vice_captain=True)
)
team7 = (
Player(24075, "Holding", "ARS", "defender", -0.60),
Player(23332, "Bellerin", "ARS", "defender", -0.76),
Player(197966, "Smith Rowe", "ARS", "midfielder", -1.24, is_vice_captain=True),
Player(137793, "Ceballos", "ARS", "midfielder", -1.08, is_captain=True),
Player(106430, "Saka", "ARS", "midfielder", -1.72),
)
team8 = (
Player(24075, "Holding", "ARS", "defender", -0.60, is_vice_captain=True),
Player(23332, "Bellerin", "ARS", "defender", -0.76),
Player(197966, "Smith Rowe", "ARS", "midfielder", -1.24),
Player(137793, "Ceballos", "ARS", "midfielder", -1.08),
Player(106430, "Saka", "ARS", "midfielder", -1.72, is_captain=True),
)
def choose_players_to_penalize(team: Team) -> PenalizedPlayers:
"""
Returns list of tuples with players ids to be penalised and amount of
points to be taken away from each of them.
Raises ValueError if defensive players count is greater than 5.
"""
defensive_players = get_defensive_players(team)
if len(defensive_players) < 2:
return ()
if len(defensive_players) == 2:
return penalize_two_players(defensive_players)
if len(defensive_players) == 3:
return penalize_three_players(defensive_players)
if len(defensive_players) == 4:
return penalize_four_players(defensive_players)
if len(defensive_players) == 5:
return penalize_five_players(defensive_players)
raise ValueError("There can't be more than 5 defensive players")
def penalize_two_players(players: Team) -> PenalizedPlayers:
"""
Chooses players ids to be penalised and amount of points to be taken away
from a team with two defensive players.
"""
sorted_players = sort_player_ids_for_penalty(players)
return ((sorted_players(1), -1))
def penalize_three_players(players: Team) -> PenalizedPlayers:
"""
Chooses players ids to be penalised and amount of points to be taken away
from a team with three defensive players.
"""
sorted_players = sort_player_ids_for_penalty(players)
return ((sorted_players(-1), -2),
(sorted_players(-2), -1))
def penalize_four_players(players: Team) -> PenalizedPlayers:
"""
Chooses players ids to be penalised and amount of points to be taken away
for a team with four defensive players.
"""
sorted_players = sort_player_ids_for_penalty(players)
return ((sorted_players(-1), -3),
(sorted_players(-2), -2),
(sorted_players(-3), -1))
def penalize_five_players(players: Team) -> PenalizedPlayers:
"""
Chooses players ids to be penalised and amount of points to be taken away
for a team with five defensive players.
"""
sorted_players = sort_player_ids_for_penalty(players)
return ((sorted_players(-1), -3),
(sorted_players(-2), -3),
(sorted_players(-3), -2),
(sorted_players(-4), -1))
def sort_player_ids_for_penalty(players: Team) -> List(PlayerId):
"""
Returns list of players ids sorted in reverse order by the following criterias:
- is_captain
- is_vice_captain
- more expensive players by pursuit points
(more "expensive" means smaller value, for example, a player with
pursuit points -0.72 is more expensive than a player with pursuit
points +1.24, so that when sorting pursuits are taken multiplied by -1)
"""
return (p.p_id for p in sorted(players, key=lambda x:
(x.is_captain, x.is_vice_captain,
-x.pursuit), reverse=True))
def get_defensive_players(players: Team) -> Team:
"""
Returns every player in the team whose position is either defender
or goalkeeper.
"""
return (p for p in players if p.position in ('goalkeeper', 'defender'))
def tests():
"""
Tests for choose_players_to_penalize()
"""
assert sorted(choose_players_to_penalize(team1)) == sorted(((24075, -2),
(139989, -1)))
assert sorted(choose_players_to_penalize(team2)) == sorted(((139989, -2),
(88472, -1)))
assert sorted(choose_players_to_penalize(team3)) == sorted(((88472, -2),
(139989, -1)))
assert sorted(choose_players_to_penalize(team4)) == sorted(((160699, -3),
(24075, -2),
(88472, -1)))
assert sorted(choose_players_to_penalize(team5)) == sorted(((24075, -3),
(139989, -3),
(23332, -2),
(160699, -1)))
assert choose_players_to_penalize(team6) == ()
assert choose_players_to_penalize(team7) == ((24075, -1))
assert choose_players_to_penalize(team8) == ((23332, -1))
print("Tests pass.")
if __name__ == '__main__':
tests()
```