Improving implementation of cards which use voting (WIP) (#7566)

* created interface for handling voting

* created class for two choice votes, refactored a card to use it

* refactored all cards which use two choice votes

* updated VoteHandler to an abstract class to encapsulate more of its functions

* refactored cards which vote for more than two things

* [CNS] Implemented Brago's Representative

* [CN2] Implemented Ballot Broker

* [CN2] Implemented Illusion of Choice

* [CNS] Implemented Grudge Keeper

* added vote outcomes

* updated implementation of Illusion of Choice to work correctly in multiples

* added test for voting

* updated implementation of extra votes

* simplified vote message handling

* Improved names, additional comments

* Votes: fixed not working getMostVoted

* Votes: added final vote results to game logs;

* Votes: added additional info for the vote choices;

* Votes: added vote step info in choose dialogs, added AI support example for Tyrant's Choice;

Co-authored-by: Oleg Agafonov <jaydi85@gmail.com>
This commit is contained in:
Evan Kranzler 2021-03-20 10:32:54 -04:00 committed by GitHub
parent 991f154cd7
commit 1cbbcddcc6
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
32 changed files with 1615 additions and 786 deletions

View file

@ -1,42 +0,0 @@
package mage.abilities.effects.common;
import java.util.UUID;
import mage.abilities.Ability;
import mage.abilities.effects.OneShotEffect;
import mage.constants.Outcome;
import mage.game.Game;
import mage.players.Player;
/**
* @author JRHerlehy
*/
public abstract class CouncilsDilemmaVoteEffect extends OneShotEffect {
protected int voteOneCount = 0, voteTwoCount = 0;
public CouncilsDilemmaVoteEffect(Outcome outcome) {
super(outcome);
}
public CouncilsDilemmaVoteEffect(final CouncilsDilemmaVoteEffect effect) {
super(effect);
}
protected void vote(String choiceOne, String choiceTwo, Player controller, Game game, Ability source) {
for (UUID playerId : game.getState().getPlayersInRange(controller.getId(), game)) {
Player player = game.getPlayer(playerId);
if (player != null) {
if (player.chooseUse(Outcome.Vote,
"Choose " + choiceOne + " or " + choiceTwo + "?",
source.getRule(), choiceOne, choiceTwo, source, game)) {
voteOneCount++;
game.informPlayers(player.getLogName() + " has voted for " + choiceOne);
} else {
voteTwoCount++;
game.informPlayers(player.getLogName() + " has voted for " + choiceTwo);
}
}
}
}
}

View file

@ -63,11 +63,15 @@ public class ChoiceColor extends ChoiceImpl {
}
public ObjectColor getColor() {
if (choice == null) {
return getColorFromString(choice);
}
public static ObjectColor getColorFromString(String colorString) {
if (colorString == null) {
return null;
}
ObjectColor color = new ObjectColor();
switch (choice) {
switch (colorString) {
case "Black":
color.setBlack(true);
break;

View file

@ -0,0 +1,41 @@
package mage.choices;
import mage.abilities.Ability;
import mage.constants.Outcome;
import mage.game.Game;
import mage.players.Player;
import java.util.Arrays;
import java.util.LinkedHashSet;
import java.util.Set;
/**
* @author TheElk801
*/
public class TwoChoiceVote extends VoteHandler<Boolean> {
private final String choice1;
private final String choice2;
private final Outcome outcome;
public TwoChoiceVote(String choice1, String choice2, Outcome outcome) {
this.choice1 = choice1;
this.choice2 = choice2;
this.outcome = outcome;
}
@Override
protected Set<Boolean> getPossibleVotes(Ability source, Game game) {
return new LinkedHashSet<>(Arrays.asList(Boolean.TRUE, Boolean.FALSE));
}
@Override
public Boolean playerChoose(String voteInfo, Player player, Player decidingPlayer, Ability source, Game game) {
return decidingPlayer.chooseUse(outcome, voteInfo, null, choice1, choice2, source, game);
}
@Override
protected String voteName(Boolean vote) {
return (vote ? choice1 : choice2);
}
}

View file

@ -0,0 +1,183 @@
package mage.choices;
import mage.abilities.Ability;
import mage.constants.Outcome;
import mage.game.Game;
import mage.game.events.VoteEvent;
import mage.game.events.VotedEvent;
import mage.players.Player;
import java.util.*;
import java.util.stream.Collectors;
/**
* @author TheElk801
*/
public abstract class VoteHandler<T> {
protected final Map<UUID, List<T>> playerMap = new HashMap<>();
protected VoteHandlerAI<T> aiVoteHint = null;
public void doVotes(Ability source, Game game) {
doVotes(source, game, null);
}
public void doVotes(Ability source, Game game, VoteHandlerAI<T> aiVoteHint) {
this.aiVoteHint = aiVoteHint;
this.playerMap.clear();
int stepCurrent = 0;
int stepTotal = game.getState().getPlayersInRange(source.getControllerId(), game).size();
for (UUID playerId : game.getState().getPlayersInRange(source.getControllerId(), game)) {
stepCurrent++;
VoteEvent event = new VoteEvent(playerId, source);
game.replaceEvent(event);
Player player = game.getPlayer(event.getTargetId());
Player decidingPlayer = game.getPlayer(event.getPlayerId());
if (player == null || decidingPlayer == null) {
continue;
}
int voteCount = event.getExtraVotes() + event.getOptionalExtraVotes() + 1;
for (int i = 0; i < voteCount; i++) {
// Decision for extra choice goes from original player, not from deciding.
// Rules from Illusion of Choice:
// If another player controls Ballot Broker, that player first takes their normal vote
// with you choosing the result, then that player decides whether they are taking the
// additional vote. If there is an additional vote, you again choose the result.
// (2016-08-23)
// Outcome.Benefit - AI must use extra vote all the time
if (i > event.getExtraVotes() && !player.chooseUse(
Outcome.Benefit, "Use an extra vote?", source, game
)) {
continue;
}
String stepName = (i > 0 ? "extra step" : "step");
String voteInfo = String.format("Vote, %s %d of %d", stepName, stepCurrent, stepTotal);
T vote;
if (!decidingPlayer.isHuman() && !decidingPlayer.isTestMode() && this.aiVoteHint != null) {
// TODO: add isComputer after PR
// ai choose
vote = this.aiVoteHint.makeChoice(this, player, decidingPlayer, source, game);
} else {
// human choose
vote = playerChoose(voteInfo, player, decidingPlayer, source, game);
}
if (vote == null) {
continue;
}
String message = voteInfo + ": " + player.getName() + " voted for " + voteName(vote);
if (!Objects.equals(player, decidingPlayer)) {
message += " (chosen by " + decidingPlayer.getName() + ')';
}
game.informPlayers(message);
this.playerMap.computeIfAbsent(playerId, x -> new ArrayList<>()).add(vote);
}
}
// show final results to players
Map<T, Integer> totalVotes = new LinkedHashMap<>();
// fill by possible choices
this.getPossibleVotes(source, game).forEach(vote -> {
totalVotes.putIfAbsent(vote, 0);
});
// fill by real choices
playerMap.entrySet()
.stream()
.flatMap(votesList -> votesList.getValue().stream())
.forEach(vote -> {
totalVotes.compute(vote, (u, i) -> i == null ? 1 : Integer.sum(i, 1));
});
Set<T> winners = this.getMostVoted();
String totalVotesStr = totalVotes.entrySet()
.stream()
.map(entry -> (winners.contains(entry.getKey()) ? " -win- " : " -lose- ") + voteName(entry.getKey()) + ": " + entry.getValue())
.sorted()
.collect(Collectors.joining("<br>"));
game.informPlayers("Vote results:<br>" + totalVotesStr);
game.fireEvent(new VotedEvent(source, this));
}
/**
* Return possible votes. Uses for info only (final results).
*
* @param source
* @param game
* @return
*/
protected abstract Set<T> getPossibleVotes(Ability source, Game game);
/**
* Choose dialog for voting. Another player can choose it (example: Illusion of Choice)
*
* @param player
* @param decidingPlayer
* @param source
* @param game
* @return
*/
protected abstract T playerChoose(String voteInfo, Player player, Player decidingPlayer, Ability source, Game game);
/**
* Show readable choice name
*
* @param vote
* @return
*/
protected abstract String voteName(T vote);
public List<T> getVotes(UUID playerId) {
return playerMap.computeIfAbsent(playerId, x -> new ArrayList<>());
}
public int getVoteCount(T vote) {
return playerMap
.values()
.stream()
.flatMap(Collection::stream)
.map(vote::equals)
.mapToInt(x -> x ? 1 : 0)
.sum();
}
public List<UUID> getVotedFor(T vote) {
return playerMap
.entrySet()
.stream()
.filter(entry -> entry.getValue() != null && entry.getValue().contains(vote))
.map(Map.Entry::getKey)
.collect(Collectors.toList());
}
public Set<T> getMostVoted() {
Map<T, Integer> map = new HashMap<>();
playerMap
.values()
.stream()
.flatMap(Collection::stream)
.forEach(t -> map.compute(t, (s, i) -> i == null ? 1 : Integer.sum(i, 1)));
int max = map.values().stream().mapToInt(x -> x).max().orElse(0);
return map
.entrySet()
.stream()
.filter(e -> e.getValue() >= max)
.map(Map.Entry::getKey)
.collect(Collectors.toSet());
}
public Set<UUID> getDidntVote(UUID playerId) {
if (playerMap.computeIfAbsent(playerId, x -> new ArrayList<>()).isEmpty()) {
return playerMap.keySet();
}
return playerMap
.entrySet()
.stream()
.filter(e -> e.getValue() != null && !e.getValue().isEmpty())
.filter(e -> !e.getValue().stream().allMatch(playerMap.get(playerId)::contains))
.map(Map.Entry::getKey)
.collect(Collectors.toSet());
}
}

View file

@ -0,0 +1,23 @@
package mage.choices;
import mage.abilities.Ability;
import mage.game.Game;
import mage.players.Player;
/**
* @author JayDi85
*/
@FunctionalInterface
public interface VoteHandlerAI<T> {
/**
* AI choosing hints for votes
*
* @param voteHandler voting handler for choosing
* @param aiPlayer player who must choose
* @param aiDecidingPlayer real player who make a choice (cab be changed by another effect, example: Illusion of Choice)
* @param aiSource
* @param aiGame
*/
T makeChoice(VoteHandler<T> voteHandler, Player aiPlayer, Player aiDecidingPlayer, Ability aiSource, Game aiGame);
}

View file

@ -437,6 +437,14 @@ public class GameEvent implements Serializable {
//combat events
COMBAT_DAMAGE_APPLIED,
SELECTED_ATTACKER, SELECTED_BLOCKER,
/* voting
targetId player who voting
sourceId sourceId of the effect doing the voting
playerId player who deciding about voting, can be changed by replace events
amount not used for this event
flag not used for this event
*/
VOTE, VOTED,
//custom events
CUSTOM_EVENT
}

View file

@ -0,0 +1,34 @@
package mage.game.events;
import mage.abilities.Ability;
import java.util.UUID;
/**
* @author TheElk801
*/
public class VoteEvent extends GameEvent {
private int extraVotes = 0; // example: you get an additional vote
private int optionalExtraVotes = 0; // example: you may vote an additional time
public VoteEvent(UUID playerId, Ability source) {
super(EventType.VOTE, playerId, source, playerId);
}
public void incrementExtraVotes() {
extraVotes++;
}
public void incrementOptionalExtraVotes() {
optionalExtraVotes++;
}
public int getExtraVotes() {
return extraVotes;
}
public int getOptionalExtraVotes() {
return optionalExtraVotes;
}
}

View file

@ -0,0 +1,24 @@
package mage.game.events;
import mage.abilities.Ability;
import mage.choices.VoteHandler;
import java.util.Set;
import java.util.UUID;
/**
* @author TheElk801
*/
public class VotedEvent extends GameEvent {
private final VoteHandler voteHandler;
public VotedEvent(Ability source, VoteHandler voteHandler) {
super(EventType.VOTED, source.getSourceId(), source, source.getControllerId());
this.voteHandler = voteHandler;
}
public Set<UUID> getDidntVote(UUID playerId) {
return voteHandler.getDidntVote(playerId);
}
}