mirror of
https://github.com/magefree/mage.git
synced 2025-12-26 13:32:06 -08:00
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:
parent
991f154cd7
commit
1cbbcddcc6
32 changed files with 1615 additions and 786 deletions
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
41
Mage/src/main/java/mage/choices/TwoChoiceVote.java
Normal file
41
Mage/src/main/java/mage/choices/TwoChoiceVote.java
Normal 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);
|
||||
}
|
||||
}
|
||||
183
Mage/src/main/java/mage/choices/VoteHandler.java
Normal file
183
Mage/src/main/java/mage/choices/VoteHandler.java
Normal 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());
|
||||
}
|
||||
}
|
||||
23
Mage/src/main/java/mage/choices/VoteHandlerAI.java
Normal file
23
Mage/src/main/java/mage/choices/VoteHandlerAI.java
Normal 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);
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
34
Mage/src/main/java/mage/game/events/VoteEvent.java
Normal file
34
Mage/src/main/java/mage/game/events/VoteEvent.java
Normal 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;
|
||||
}
|
||||
}
|
||||
24
Mage/src/main/java/mage/game/events/VotedEvent.java
Normal file
24
Mage/src/main/java/mage/game/events/VotedEvent.java
Normal 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);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue