egonSchiele / dominion

A Dominion simulator in Haskell
Other
111 stars 11 forks source link

How to deal with more complicated reactions/effects #9

Open amalloy opened 10 years ago

amalloy commented 10 years ago

In expansion sets, there are a lot of cards that are more complicated than the things in Base: especially interesting are cards that "react" by interrupting a play in progress, doing something else, and then letting the play continue. Two interesting examples are Market Square and Haggler. Market Square can (optionally) fire any time it's in your hand when one of your cards is trashed, which might be during your turn or an opponent's turn. Haggler isn't technically a Reaction card, but when played it creates lasting effects with the "while this is in play, when you buy a card, gain a card costing less than it which is not a victory card" wording.

Cards like this aren't very easy to deal with in the current setup, where a lot of the engine logic directly manipulates [Card] values representing decks, hands, and piles. How can a Market Square know to trigger if, eg, Moneylender just calls delete CA.copper hand?

A solution that occurred to me tonight is to have these card effects be reified as values, and have a continuation associated with them: "trash a copper from my hand, and when you're done with that give me +3 coins" can be represented as Effect (Trash CA.copper) (\_ -> (AddCoins 3)). This Effect value gets passed to the engine, which processes any eligible on-trash effects and then calls the continuation to allow the Moneylender to continue its processing.

And I think you can wrap that plumbing up in a new monad (I don't think it's actually close to the Continuation monad, but I could be wrong), so that you can instead write something like:

resolve moneylender =
  when (CA.copper `elem` hand) $ do
    trash CA.copper
    addCoins 3

I think this general approach even allows for crazy cards like Possession, which will be intercepting each Gain effect to change what player gains the card.

So how does this sound? I think it requires some pretty big changes to the existing code, so definitely requires careful consideration.

amalloy commented 10 years ago

On further reflection I think this can probably be handled by just the State monad, with a newtype wrapper around it so that it's impossible for functions to change things willy-nilly without going through the appropriate reaction-processing callbacks.

egonSchiele commented 10 years ago

I think you're right...there are already a couple of reaction cards that don't work completely with the current architecture. However this requires a big refactor, which I don't have time for right now :/ If you want to take it on, feel free! I'm always available for merging pull requests.