-
Notifications
You must be signed in to change notification settings - Fork 0
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
Implement actions and global refactoring #24
Implement actions and global refactoring #24
Conversation
CardBlueprint and CardObject seems to play similar roles, the only differences are related to their attributes, so it is more optimal to have one class with optional attributes and properties to easily get their type
…k if the game is over
…ties and a method check_if_game_is_over
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A first few comments from what I looked into
blocker_player.life_points -= attacker_power | ||
|
||
# If the player has no more life points, it loses the game | ||
if blocker_player.life_points <= 0: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We need to keep in mind that, during the damage step, all damage is dealt simultaneously, we don't check if a creature or player is dead between the damage dealt by creature 1 and creature 2 (unless one of the two has first strike or double strike)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok understood, is it better like this?
2d284f4
(I know there is a lot of code duplication here, I will rewrite the code more elegantly once we agree on the implementation)
# TODO: Other players can respond to the action | ||
|
||
# Resolve the stack | ||
# TODO: Is it correct to resolve the whole stack at once? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nope, we resolve a single element
Then, both players need to pass priority consecutively to resolve the next element
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok thanks. Actually, I have in mind that for each subclass of Action, we could list the possible "counter action", i.e. the other Action subclasses which correspond to possible reactions of the other players. So I assume that for instance, in this particular example, when an element on the stack is resolved the players can either play an instant, or activate an ability or pass priority (I might be wrong but I hope you get the general idea of listing possible answers). In that case, I could add a method in the while loop here to check for actions from the players before resolving the next element
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't know that it makes sense to talk about counter actions
because the possibilities are not that different when a spell is on the stack VS not (except that sorcery speed things cannot be done even during the main phase, and that spells and abilities that targets stuff on the stack, like counterspells, can be played since there are legal targets)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok I see. I guess what is missing currently is that instead of just asking the current player to do actions we should have a for loop on the players (where the current player is the first one) and each player can choose an action. With this process, it would allow other players to do actions depending on the action of the current player while keeping the implementation generic, without this concept of "counter action"
magic_the_gathering/actions/tap.py
Outdated
player_board = game_state.zones[ZonePosition.BOARD][self.player_index] | ||
assert self.card_uuid in player_board.keys() | ||
player_card = player_board[self.card_uuid] | ||
# FIXME: Increase mana pool if land |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd say no: a land can be tapped by another effect without producing mana
For instance, with Ice https://scryfall.com/card/mh2/290/fire-ice someone (btw hello split cards that we probably won't support :p)
It's the effect of the activated ability t: add W
from a plains for instance that produces mana, not the fact that it gets tapped
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Exactly like with Llanowar Elves https://scryfall.com/card/gnt/46/llanowar-elves
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes exactly, it is the activated ability that produces mana, however we currently don't support abilities at all, this is why I marked it as a FIXME 😅 Yesterday I was hesitating, like maybe for now we could create mana as the result of tapping lands, otherwise we need to start thinking about the logic for abilities. I don't know which way is best
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
split cards
WTF? One card actually being two cards? 😁
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh sure we could have land tapping = mana produced for now
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I did a naive implementation of this in this commit: 38e54d7
class CombatDeclareAttackersPhase(PhaseWherePlayersGetPriority): | ||
@staticmethod | ||
def list_possible_actions(game_state: GameState) -> List[Action]: | ||
return DeclareAttackerAction.list_possible_actions(game_state=game_state) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If I understood the code well, there is an issue here: here this list is a list of this creature attacks this player
like
- creature a attacks player B
- creature a attacks player C
- creature b attacks player B
- creature b attacks player C
And then the random IA will choose one of these.
But isn't what we want to choose at random a set of "this creature attacks this player" rather than a single one?
Like, option 1 could be "a attacks B and b attacks C"
Also, we need to support options where a creature does not attack, like the set "a attacks B"
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes you are right, the players needs to be able to declare several attackers. The way I chose to do it (because I found it easier to implement although I do understand that it is not clear without explanations) is to use a while loop in the run method:
def run(self, game_state: GameState) -> GameState:
while True:
possible_actions = CombatDeclareAttackersPhase.list_possible_actions(game_state)
current_player = game_state.current_player
action = current_player.choose_action(
game_state=game_state,
possible_actions=possible_actions,
)
if action is None:
break
game_state = action.execute(game_state)
return game_state
Here is the process:
- Current player gets to choose one
DeclareAttackerAction
action among the possible actions - If the chosen action is None, we consider the player stops declaring attackers (btw I realize that I did not include this in the implementation of the Player, so you are right it would be nice to have an action to mean "I don't want to declare more attackers")
- Otherwise, the chosen action is applied. You can see how I implement the declaration of the attackers in
magic_the_gathering/actions/declare_attacker.py
. - Now we enter the loop again, we list the possible actions (which is different since the previous action affected the game state so that one card is already an attacker and thus cannot be chosen again)
- The current player gets to choose whether it declares one more attacker
- Etc...
For me it was simpler to implement it this way. The DeclareAttackerAction
is more "atomic", we support the declaration of several attackers and it means that on the player side everything looks like choosing one action at a time (which is simpler than deciding a set of actions at once).
However, if you find issues with this implementation, we can discuss of course 🙂
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think I found how to handle the "I don't want to declare attackers anymore":
@staticmethod
def list_possible_actions(game_state: GameState) -> List[Action]:
possible_actions = [None]
possible_actions.extend(DeclareAttackerAction.list_possible_actions(game_state=game_state))
return possible_actions
If there is one None
element in the list, then the player can choose it and then it will be interpreted in the run method as I explained before 🙂
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I added None
to the possible actions in all the relevant phases. Of course we can change this if you think there is a better way 😉
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah ok
One problem I can think of with this approach is that it seems that there is a need to check if a "sub-battle" is legal, with a sub-battle being one attacker being blocked by one or more blockers.
Ex: menace
makes a creature only be blockable by 2 creatures or more.
This makes it so some combination of blocks may be individually legal, but not legal when looked at together.
And menace is common.
Like an attacker with menace attacks, I have a single creature, I list the possible first blocks I could take, I block it with one creature, but then I can't block it with more, and my blocking combination is illegal.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, I see! I was not aware of that. I will create an issue for this, thank you 🙂
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could you describe what you did in I am wondering whether my current code is not already doing something similar (using the logic of while loop I described here #24 (comment)). Actually, this is also to avoid having to compute all the possible sets of attackers or blockers that I decided to use atomic actions and a while loop to iteratively declare them and check progressively what are the possible actions left. What do you think? |
Here is some background about my thinking process:
def execute(self, game_state: GameState) -> GameState:
game_state = self._execute(game_state)
game_state.action_history.append(self)
game_state.check_if_game_is_over()
return game_state
In this implementation, most of the complexity is in the Of course here, we don't implement abilities and also we miss the possible "reactions" of players. However, I think they are feasible:
|
Regarding the mana, there are several things which are missing:
|
Another thing which is missing in the current implementation. The stack is only used during the Similarly, the stack is resolved only during the |
3 -> The mana pool is emptied when going from any phase to the next one |
The stack can get used and resolved basically anytime players can get priority, and resolve one element at a time |
Yes, that's roughly what my code does, coming from the perspective that individual blocks must be legal, but "sub-battles" too, like with |
It was simpler for me to treat several issues at the same time to have the global picture in mind, sorry for the big PR 😁
Implement these issues: