Skip to content

Improved game._is_action_allowed() #236

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions botbowl/ai/proc_bot.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,10 +97,10 @@ def act(self, game: Game):
assert action is not None

# handle illegal action. if handle_illegal_action() returns a new legal action, return that.
if not game._is_action_allowed(action):
if not game.is_action_allowed(action):
new_action = self.handle_illegal_action(game, action)
assert isinstance(new_action, Action)
assert game._is_action_allowed(new_action)
assert game.is_action_allowed(new_action)
return new_action

return action
Expand Down
63 changes: 37 additions & 26 deletions botbowl/core/game.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,30 @@
This module contains the Game class, which is the main class and interface used to interact with a game in botbowl.
"""
import itertools
from copy import deepcopy
from typing import Tuple, Union, Any

from botbowl.core.forward_model import Trajectory, MovementStep, Step
from botbowl.core.load import *
from botbowl.core.procedure import *
from botbowl.core.forward_model import Trajectory, MovementStep, Step
from copy import deepcopy
from typing import Optional, Tuple, List, Union, Any


class InvalidActionError(Exception):
pass


class BoolWithMsg:
def __init__(self, evaluate_as, msg=""):
self.msg = msg
self.evaluate_as = evaluate_as

def __bool__(self):
return self.evaluate_as

def __repr__(self):
return f"BoolWithMsg({self.evaluate_as}, msg='{self.msg}')"


class Game:
replay: Optional[Replay]
game_id: str
Expand Down Expand Up @@ -275,25 +287,26 @@ def _end_game(self) -> None:
self.replay.dump(self)

def _is_action_allowed(self, action: Action) -> bool:
"""Calls the function below, converts to bool. Left here for legacy reasons"""
return bool(self.is_action_allowed(action))

def is_action_allowed(self, action: Action) -> BoolWithMsg:
"""
Checks whether the specified action is allowed by comparing to actions in self.state.available_actions.
:param action:
:return: True if the specified actions is allowed.
"""
if action is None:
return True
return BoolWithMsg(True)
for action_choice in self.state.available_actions:
if action.action_type == action_choice.action_type:
# Type checking
if type(action.action_type) is not ActionType:
print("Illegal action type: ", type(action.action_type))
return False
return BoolWithMsg(False, f"Illegal action type: {type(action.action_type)}")
if action.player is not None and not isinstance(action.player, Player):
print("Illegal player type: ", type(action.action_type), action, self.state.stack.peek())
return False
return BoolWithMsg(False, f"Illegal player type: {type(action.action_type)}, {action}, {self.state.stack.peek()}")
if action.position is not None and not isinstance(action.position, Square):
print("Illegal position type:", type(action.position), action.action_type.name)
return False
return BoolWithMsg(False, f"Illegal position type: {type(action.position)}, {action.action_type.name}")
# Check if player argument is used instead of position argument
if len(action_choice.players) == 0 and action.player is not None and action.position is None:
action.position = action.player.position
Expand All @@ -303,19 +316,18 @@ def _is_action_allowed(self, action: Action) -> bool:
# Check player argument
if len(action_choice.players) > 1 and action.player not in action_choice.players:
if action.player is None:
print("Illegal player: None")
return BoolWithMsg(False, "Illegal player: None")
else:
print("Illegal player:", action.player.to_json(), action.action_type.name)
return False
return BoolWithMsg(False, f"Illegal player: {action.player.to_json()}, {action.action_type.name}")
# Check position argument
if len(action_choice.positions) > 0 and action.position not in action_choice.positions:
if action.position is None:
print("Illegal position: None")
return BoolWithMsg(False, "Illegal position: None")
else:
print("Illegal position:", action.position.to_json(), action.action_type.name)
return False
return True
return False
return BoolWithMsg(False, f"Illegal position: {action.position.to_json()}, {action.action_type.name}")

return BoolWithMsg(True)
return BoolWithMsg(False, f"Illegal action type: {action.action_type}")

def _safe_act(self) -> Optional[Action]:
"""
Expand Down Expand Up @@ -403,7 +415,7 @@ def _one_step(self, action: Action) -> bool:
return True # Game needs user input
else:
# Only allowed actions
if not self._is_action_allowed(action):
if not self.is_action_allowed(action):

if type(action) is Action:
raise InvalidActionError(
Expand Down Expand Up @@ -704,22 +716,22 @@ def add_or_skip_turn(self, turns: None) -> None:
"""
Adds or removes a number of turns from the current half. This method will raise an assertion error if the turn
counter goes to a negative number.
:param turns: The number of turns to add (if positive) or remove (if negative).
:param turns: The number of turns to add (if positive) or remove (if negative).
"""
for team in self.state.teams:
team.state.turn += turns
assert team.state.turn >= 0

def get_player(self, player_id: str) -> Optional[Player]:
"""
:param player_id:
:param player_id:
:return: Returns the player with player_id
"""
return self.state.player_by_id[player_id]

def get_player_at(self, position: Square) -> Optional[Player]:
"""
:param position:
:param position:
:return: Returns the player at pos else None.
"""
return self.state.pitch.board[position.y][position.x]
Expand Down Expand Up @@ -776,7 +788,7 @@ def is_home_team(self, team: Team) -> bool:

def get_opp_team(self, team: Team) -> Team:
"""
:param team:
:param team:
:return: The opponent team of team.
"""
return self.state.home_team if self.state.away_team == team else self.state.away_team
Expand All @@ -786,7 +798,7 @@ def get_dugout(self, team: Team) -> Dugout:

def get_reserves(self, team: Team) -> List[Player]:
"""
:param team:
:param team:
:return: The reserves in the dugout of this team.
"""
return self.get_dugout(team).reserves
Expand Down Expand Up @@ -816,8 +828,7 @@ def current_turn(self) -> Optional[Turn]:
"""
:return: The top-most Turn procedure in the stack.
"""
for i in reversed(range(self.state.stack.size())):
proc = self.state.stack.items[i]
for proc in reversed(self.state.stack.items):
if isinstance(proc, Turn):
return proc
return None
Expand Down
6 changes: 3 additions & 3 deletions examples/scripted_bot_example.py
Original file line number Diff line number Diff line change
Expand Up @@ -216,7 +216,7 @@ def turn(self, game):
# Execute planned actions if any
while len(self.actions) > 0:
action = self._get_next_action()
if game._is_action_allowed(action):
if game.is_action_allowed(action):
return action

# Split logic depending on offense, defense, and loose ball - and plan actions
Expand Down Expand Up @@ -518,7 +518,7 @@ def player_action(self, game):
# Execute planned actions if any
while len(self.actions) > 0:
action = self._get_next_action()
if game._is_action_allowed(action):
if game.is_action_allowed(action):
return action

ball_carrier = game.get_ball_carrier()
Expand Down Expand Up @@ -735,7 +735,7 @@ def path_to_move_actions(game: botbowl.Game, player: botbowl.Player, path: Path,

final_action = Action(action_type, position=path.get_last_step())

if game._is_action_allowed(final_action):
if game.is_action_allowed(final_action):
return [final_action]
else:
actions = []
Expand Down