I am going to answer you a little bit. One of the cool things about two forum members making the new implementation should be a closer distance between the fans and the development. So far I've been very silent, but that is mostly due to time constraints. Also I don't really know where answering you leads to. One of the bad things that might happen is that it leads to a lot of other questions that people also want answered, and at that point I'm just going to say no, not now, now I'm actually going to continue making the game.
People often assume the difficult part about Dominion is in the cards, but so far it hasn't been. By far the most time consuming was setting up the
Context. It isn't a lot of code, but I sort of had to figure out how Dominion actually works. The rules aren't always clear about this, and where they are they usually describe what should happen, not the things causing these consequences.
Anyway, as it turns out the Dominion Context has a Stack. Each element of the stack has a Map that assigns each Player a list of Abilities, and could have a cause (also an Ability). An Ability is basically just 'a piece of code' and apart from being executed it can also be cancelled, when one of its tracked cards get moved, covered up or shuffled due to something else.
The Stack is because if something is happening and it triggers something else, we first resolve something else before returning to the first thing.
The elements in the stack don't have just one list but a list for every player because of the turn-order resolution rule.
The cause is there because if something happens (say when-trash-a-card | when-your-turn-starts) and further down its stack some new trigger is set up (you draw a market square | your summoned hireling hits play) it has to still respond to the trigger-being-resolved.
- Are there any card interactions that have a true corner-case implementation, where the engine produces the wrong effect when A and B interact, but there's no feasible way to correct the individual components to get the right effect?
No. If this is the case, that simply means the engine is wrong. It has been, and maybe it still is, but if I find that out I will try to correct the engine (again), never start coding specific interactions.
I'll just give you the current implementation of two cards. One very simple, one a bit more complicated.
public class Margrave extends Card {
public Margrave(int index, CardZone cardZone) {
super(index, CardObjectName.MARGRAVE, cardZone);
}
public void onPlay(Context context, Player player, PlayCardAbility playCardAbility) throws SuspendExecution {
player.drawCards(context, getCardObjectAssociation(), 3);
player.addBuys(1);
for (Player victim : playCardAbility.getVictims()) {
victim.drawCards(context, getCardObjectAssociation(), 1);
victim.discardDownTo(context, getCardObjectAssociation(), 3);
}
}
}
public class Beggar extends ReactionCard {
private final List<CardMode> CARD_MODES = Arrays.asList (CardMode.BEGGAR_TOPDECK_FIRST, CardMode.BEGGAR_DISCARD_PILE_FIRST);
public Beggar(int index, CardZone cardZone) {
super(index, CardObjectName.BEGGAR, cardZone);
}
@Override
public void onPlay(Context context, Player player, PlayCardAbility playCardAbility) throws SuspendExecution {
for (int i = 0; i < 3; i++) {
player.gain(context, getMe(), CardObjectName.COPPER, player.getHand());
}
}
@Override
protected boolean reactsTo(Event event, Context context) {
boolean inHand = getZone().getZoneName() == ZoneName.HAND;
boolean isWhen = event.getEventType() == EventType.WHEN;
boolean isAttack = event.getAbility() instanceof PlayAttackAbility;
if (!inHand || !isWhen || !isAttack) return false;
PlayAttackAbility playAttackAbility = (PlayAttackAbility) event.getAbility();
return playAttackAbility.getPlayer() != getPlayer();
}
@Override
protected void createReaction(Event event, Context context, AbilityCollector abilityCollector) {
abilityCollector.add(new BeggarAbility(getPlayer()));
}
private class BeggarAbility extends ReactionAbility {
public BeggarAbility(Player player) {
super(getMe(), player);
}
@Override
public void resolve(Context context) throws SuspendExecution {
player.discard(context, getCardObjectAssociation(), player.getHand(), getMe());
NamedPile silverPile = context.getCommonPiles().getActualPile(CardObjectName.SILVER);
CardMode cardMode = CardMode.BEGGAR_TOPDECK_FIRST;
if (silverPile.size() == 1) {
cardMode = context.ask(new WhatCardMode(CARD_MODES));
}
if (cardMode == CardMode.BEGGAR_TOPDECK_FIRST) {
player.gain(context, getMe(), CardObjectName.SILVER, player.getDrawPile());
player.gain(context, getMe(), CardObjectName.SILVER);
} else {
player.gain(context, getMe(), CardObjectName.SILVER);
player.gain(context, getMe(), CardObjectName.SILVER, player.getDrawPile());
}
}
}
}