Jack
Jack

Reputation: 415

Python OOP - Soccer Simulation

I'm new to OOP. I'd like to simulate a soccer match. How do I access Play instance variables in the Player/Offender/Defender classes? If another structure is better, please help.

class Player:
    def __init__(self, x, y):
        self.x = x
        self.y = y

    def move_to(self, x, y):
        self.x = (self.x + x) / 2
        self.y = (self.y + y) / 2

    ## Loop through Play.Players to find nearest. How do I access Play.Players?
    def nearest(self):
        return nearest

class Offender(Player):
    def __init__(self, x, y):
        super().__init__(x, y)

    # Move to ball.
    def strategy():
        ball_x = Play.ball_x # how do I access Play.ball_x
        ball_y = Play.ball_y # how do I access Play.ball_y
        self.move_to(ball_x, ball_y)

class Defender(Player):
    def __init__(self, x, y):
        super().__init__(x, y)

    # Move to nearest player
    def strategy(self):
        nearest = self.nearest()
        self.move_to(nearest)          

class Play:
    def __init__(self, offense_players, defense_players, ball_x, ball_y):
        self.offense = [Offender(player) for player in offense_players]
        self.defense = [Defender(player) for player in defense_players]
        self.players = self.offense + self.defense
        self.ball_x = ball_x
        self.ball_y = ball_y

    def simulate(self):
        while True:
            for player in self.players:
                player.strategy()

if __name__ == "__main__":
    Play().simulate()

Instead of having Offender and Defender classes, I have one for each position, i.e. Striker(Player), Midfielder(Player), Goalie(Player), etc. which is why I'd like to store their respective strategy in their class and not in the Play class.

Upvotes: 4

Views: 1414

Answers (3)

Jonathan Herrera
Jonathan Herrera

Reputation: 6144

I just elaborate the idea from the comment of martineau: We just pass the Play instance as argument to the relevant methods of Player. I also wrote out a very first draft for the nearest() method, but you might want to improve its logic. I was just drafting this to demonstrate how you could solve your OOP design problem.

import typing


class Player:
    def __init__(self, x: float, y: float):
        self.x = x
        self.y = y

    def move_to(self, x: float, y: float) -> None:
        self.x = (self.x + x) / 2
        self.y = (self.y + y) / 2

    def nearest(self, play: "Play") -> "Player":
    # This must yet be adapted to handle the edge case of two players 
# having equal distance to the current player. You didn't specify the 
# desired output for that case, hence I just ignored that scenario for now.
        return min([
            p for p in play.players 
            if p.x != self.x or p.y != self.y, # this is to 
# exclude the player itself. 
# This is a buggy logic, because it relies on an assumption that is not  
# validated in the code (the assumption that two different players never 
# have identical coordinates). You might want to introduce a uniqe `id` 
# instance variable to the Player class, to handle this identification in a 
# clean way.
            ],
            key=lambda p: (self.x - p.x)**2 + (self.y - p.y)**2
        ) 


class Offender(Player):
    def __init__(self, x: float, y: float):
        super().__init__(x, y)

    # Move to ball.
    def strategy(self, play: "Play") -> None:
        self.move_to(play.ball_x, play.ball_y)


class Defender(Player):
    def __init__(self, x: float, y: float):
        super().__init__(x, y)

    # Move to nearest player
    def strategy(self, play: "Play") -> None:
        self.move_to(self.nearest(play=play)          


class Play:
    def __init__(
    self, 
    offense_players: typing.List["Offender"], 
    defense_players: typing.List["Defender"],
    ball_x: float, 
    ball_y: float,
):
        self.offense = offense_players
        self.defense = defense_players
        self.players = self.offense + self.defense
        self.ball_x = ball_x
        self.ball_y = ball_y

    def simulate(self) -> None:
        while True:  # this is still a bad condition, you might want to change this. However you didn't specify your desired logic, so I didn't change it.
            for player in self.players:
                player.strategy(self, play=self)

Upvotes: 1

Manoj Bisht
Manoj Bisht

Reputation: 31

Not sure how much this will be helpful for you, as the implementation is in C++

You can checkout my implementation for a similar problem statement https://github.com/rimpo/footballcpp

For understanding the arcade game framework implementation for which the above bot was written, checkout http://richard-shepherd.github.io/coding-world-cup/index.html

Upvotes: 2

modesitt
modesitt

Reputation: 7210

I would do the following:

keep track of game state in another (data) class:

class GameState:

    def __init__(self, offense_players, defense_players, ball_x, ball_y):
        self.offense = offense_players
        self.defense = defense_players
        self.players = self.offense + self.defense
        self.ball_x = ball_x
        self.ball_y = ball_y

You may even wish to use python3.7 dataclasses (and some other features) for this (although it is not at all necessary)

from dataclasses import dataclass
from typing import List 


@dataclass 
class GameState:

    offense: List[Offender]
    defense: List[Defender]
    ball_x: float 
    ball_y: float 

    @property 
    def players(self):
        return offense + defense 

Players then take this state in their strategy and are expected to update their internal state (like position). nearest player is implemented by taking the minimum l2 distance between other players using the key argument to min that takes a function of another player, p which is written using lambda.

class Player:
    def __init__(self, x, y):
        self.x = x
        self.y = y

    def move_to(self, x, y, other_players):
        self.x = (self.x + x) / 2
        self.y = (self.y + y) / 2

    def nearest(self):
        return nearest

class Offender(Player):
    def __init__(self, x, y):
        super().__init__(x, y)

    # Move to ball.
    def strategy(self, game_state):
        ball_x = game_sate.ball_x # how do I access Play.ball_x
        ball_y = game_state.ball_y # how do I access Play.ball_y
        self.move_to(ball_x, ball_y)

class Defender(Player):
    def __init__(self, x, y):
        super().__init__(x, y)

    # Move to nearest player
    def strategy(self, game_state):
        # we assume you are moving to offensive players - which 
        # you will not be apart of 
        nearest = min(
            game_state.offense
            key=lambda p: (
                (self.x - p.x) **2 + (self.y - p.y) ** 2
            ) ** (1/2)  # take the l2 norm to find the "closest" player to you
        )
        self.move_to(nearest.x, nearest.y)      

Then, play the game

class Play:

    def __init__(self, game_state):
        self.game_state = game_state

    def simulate(self):
        while True:
            for player in self.game_state.players:
                player.strategy(self.game_state)

if __name__ == "__main__":
    Play(GameState(
        [Offender(-1, 0), Offender(-1, -1), ...]
        [Defender(1, 0), Offender(1, -1), ...]
    )).simulate()

You could then implement some actual players

class TiernaDavidson(Defender):

    def strategy(self, *args, **kwargs):
        return better_strategy(*args, **kwargs)

You will have to ask her for the implementation of better_strategy ;)

Upvotes: 1

Related Questions