Add Smoothed London Mulligan option (#10965)

* Add Smoothed London Mulligan (similar to but weaker than MTGA's)

* Make SmoothedLondonMulligan extend LondonMulligan instead of copying code

* modified to be have no effect within +1/-1 of the expected lands
fixed tests by always putting nonchosen hand on the bottom

* Inherit the primary mulligan logic as well, add comments

* Make drawHand public and part of Mulligan, use it on opening 7
use Card::isLand instead of reimplementing it, remove unused imports
Use standard spacing

* Better account for half-land MDFCs

* Don't count TDFCs as half-lands

* Remove "crossover_point" calculation to make algorithm clearer

* Genericize the tests, undo changed access that's no longer needed, unbox bool

* Use standard case in function naming

* Add Override

* Add mulligan type to TableView info, add tourneyMatchOptions local variable
This commit is contained in:
ssk97 2023-08-27 12:08:27 -07:00 committed by GitHub
parent a7c77a8895
commit c50e913398
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 153 additions and 41 deletions

View file

@ -7,7 +7,9 @@ import mage.game.Seat;
import mage.game.Table; import mage.game.Table;
import mage.game.draft.Draft; import mage.game.draft.Draft;
import mage.game.draft.DraftOptions; import mage.game.draft.DraftOptions;
import mage.game.match.MatchOptions;
import mage.game.match.MatchPlayer; import mage.game.match.MatchPlayer;
import mage.game.mulligan.MulliganType;
import mage.game.tournament.TournamentPlayer; import mage.game.tournament.TournamentPlayer;
import java.io.Serializable; import java.io.Serializable;
@ -99,6 +101,9 @@ public class TableView implements Serializable {
addInfo.append("Wins:").append(table.getMatch().getWinsNeeded()); addInfo.append("Wins:").append(table.getMatch().getWinsNeeded());
addInfo.append(" Time: ").append(table.getMatch().getOptions().getMatchTimeLimit().toString()); addInfo.append(" Time: ").append(table.getMatch().getOptions().getMatchTimeLimit().toString());
addInfo.append(" Buffer: ").append(table.getMatch().getOptions().getMatchBufferTime().toString()); addInfo.append(" Buffer: ").append(table.getMatch().getOptions().getMatchBufferTime().toString());
if (table.getMatch().getOptions().getMulliganType() != MulliganType.GAME_DEFAULT){
addInfo.append(" Mulligan: \"").append(table.getMatch().getOptions().getMulliganType().toString()).append("\"");
}
if (table.getMatch().getFreeMulligans() > 0) { if (table.getMatch().getFreeMulligans() > 0) {
addInfo.append(" FM: ").append(table.getMatch().getFreeMulligans()); addInfo.append(" FM: ").append(table.getMatch().getFreeMulligans());
} }
@ -150,10 +155,14 @@ public class TableView implements Serializable {
if (TableState.WAITING.equals(table.getState())) { if (TableState.WAITING.equals(table.getState())) {
stateText.append(" (").append(table.getTournament().getPlayers().size()).append('/').append(table.getNumberOfSeats()).append(')'); stateText.append(" (").append(table.getTournament().getPlayers().size()).append('/').append(table.getNumberOfSeats()).append(')');
} }
infoText.append(" Time: ").append(table.getTournament().getOptions().getMatchOptions().getMatchTimeLimit().toString()); MatchOptions tourneyMatchOptions = table.getTournament().getOptions().getMatchOptions();
infoText.append(" Buffer: ").append(table.getTournament().getOptions().getMatchOptions().getMatchBufferTime().toString()); infoText.append(" Time: ").append(tourneyMatchOptions.getMatchTimeLimit().toString());
if (table.getTournament().getOptions().getMatchOptions().getFreeMulligans() > 0) { infoText.append(" Buffer: ").append(tourneyMatchOptions.getMatchBufferTime().toString());
infoText.append(" FM: ").append(table.getTournament().getOptions().getMatchOptions().getFreeMulligans()); if (tourneyMatchOptions.getMulliganType() != MulliganType.GAME_DEFAULT){
infoText.append(" Mulligan: \"").append(tourneyMatchOptions.getMulliganType().toString()).append("\"");
}
if (tourneyMatchOptions.getFreeMulligans() > 0) {
infoText.append(" FM: ").append(tourneyMatchOptions.getFreeMulligans());
} }
if (table.getTournament().getTournamentType().isLimited()) { if (table.getTournament().getTournamentType().isLimited()) {
infoText.append(" Constr.: ").append(table.getTournament().getOptions().getLimitedOptions().getConstructionTime() / 60).append(" Min."); infoText.append(" Constr.: ").append(table.getTournament().getOptions().getLimitedOptions().getConstructionTime() / 60).append(" Min.");
@ -162,10 +171,10 @@ public class TableView implements Serializable {
DraftOptions draftOptions = (DraftOptions) table.getTournament().getOptions().getLimitedOptions(); DraftOptions draftOptions = (DraftOptions) table.getTournament().getOptions().getLimitedOptions();
infoText.append(" Pick time: ").append(draftOptions.getTiming().getShortName()); infoText.append(" Pick time: ").append(draftOptions.getTiming().getShortName());
} }
if (table.getTournament().getOptions().getMatchOptions().isRollbackTurnsAllowed()) { if (tourneyMatchOptions.isRollbackTurnsAllowed()) {
infoText.append(" RB"); infoText.append(" RB");
} }
if (table.getTournament().getOptions().getMatchOptions().isPlaneChase()) { if (tourneyMatchOptions.isPlaneChase()) {
infoText.append(" PC"); infoText.append(" PC");
} }
if (table.getTournament().getOptions().isWatchingAllowed()) { if (table.getTournament().getOptions().isWatchingAllowed()) {

View file

@ -10,10 +10,15 @@ import java.util.stream.Collectors;
import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertEquals;
public class LondonMulliganTest extends MulliganTestBase { public class LondonMulliganTest extends MulliganTestBase {
protected MulliganType getMullType() {
return MulliganType.LONDON;
}
protected int getCardsPerMull() {
return 7;
}
@Test @Test
public void testLondonMulligan_NoMulligan() { public void testLondonMulligan_NoMulligan() {
MulliganScenarioTest scenario = new MulliganScenarioTest(MulliganType.LONDON, 0); MulliganScenarioTest scenario = new MulliganScenarioTest(getMullType(), 0);
Set<UUID> hand1 = new HashSet<>(); Set<UUID> hand1 = new HashSet<>();
scenario.mulligan(() -> { scenario.mulligan(() -> {
scenario.assertSizes(7, 33); scenario.assertSizes(7, 33);
@ -28,7 +33,7 @@ public class LondonMulliganTest extends MulliganTestBase {
@Test @Test
public void testLondonMulligan_OneMulligan() { public void testLondonMulligan_OneMulligan() {
MulliganScenarioTest scenario = new MulliganScenarioTest(MulliganType.LONDON, 0); MulliganScenarioTest scenario = new MulliganScenarioTest(getMullType(), 0);
Set<UUID> hand1 = new HashSet<>(); Set<UUID> hand1 = new HashSet<>();
Set<UUID> hand2 = new HashSet<>(); Set<UUID> hand2 = new HashSet<>();
List<UUID> discarded = new ArrayList<>(); List<UUID> discarded = new ArrayList<>();
@ -41,14 +46,14 @@ public class LondonMulliganTest extends MulliganTestBase {
scenario.discardBottom(count -> { scenario.discardBottom(count -> {
scenario.assertSizes(7, 33); scenario.assertSizes(7, 33);
assertEquals(1, count); assertEquals(1, count);
assertEquals(hand1, new HashSet<>(scenario.getLibraryRangeSize(26, 7))); assertEquals(hand1, new HashSet<>(scenario.getLibraryRangeSize(33-getCardsPerMull(), 7)));
scenario.getHand().stream().limit(count).forEach(discarded::add); scenario.getHand().stream().limit(count).forEach(discarded::add);
remainingHand.addAll(Sets.difference(scenario.getHand(), new HashSet<>(discarded))); remainingHand.addAll(Sets.difference(scenario.getHand(), new HashSet<>(discarded)));
return discarded; return discarded;
}); });
scenario.mulligan(() -> { scenario.mulligan(() -> {
scenario.assertSizes(6, 34); scenario.assertSizes(6, 34);
assertEquals(hand1, new HashSet<>(scenario.getLibraryRangeSize(26, 7))); assertEquals(hand1, new HashSet<>(scenario.getLibraryRangeSize(33-getCardsPerMull(), 7)));
assertEquals(remainingHand, scenario.getHand()); assertEquals(remainingHand, scenario.getHand());
hand2.addAll(scenario.getHand()); hand2.addAll(scenario.getHand());
return false; return false;
@ -56,7 +61,7 @@ public class LondonMulliganTest extends MulliganTestBase {
scenario.run(() -> { scenario.run(() -> {
scenario.assertSizes(6, 34); scenario.assertSizes(6, 34);
assertEquals(remainingHand, new HashSet<>(scenario.getHand())); assertEquals(remainingHand, new HashSet<>(scenario.getHand()));
assertEquals(hand1, new HashSet<>(scenario.getLibraryRangeSize(26, 7))); assertEquals(hand1, new HashSet<>(scenario.getLibraryRangeSize(33-getCardsPerMull(), 7)));
assertEquals(hand2, scenario.getHand()); assertEquals(hand2, scenario.getHand());
assertEquals(discarded, scenario.getNBottomOfLibrary(1)); assertEquals(discarded, scenario.getNBottomOfLibrary(1));
}); });
@ -64,7 +69,7 @@ public class LondonMulliganTest extends MulliganTestBase {
@Test @Test
public void testLondonMulligan_TwoMulligan() { public void testLondonMulligan_TwoMulligan() {
MulliganScenarioTest scenario = new MulliganScenarioTest(MulliganType.LONDON, 0); MulliganScenarioTest scenario = new MulliganScenarioTest(getMullType(), 0);
Set<UUID> hand1 = new HashSet<>(); Set<UUID> hand1 = new HashSet<>();
Set<UUID> hand2 = new HashSet<>(); Set<UUID> hand2 = new HashSet<>();
Set<UUID> hand3 = new HashSet<>(); Set<UUID> hand3 = new HashSet<>();
@ -78,23 +83,23 @@ public class LondonMulliganTest extends MulliganTestBase {
scenario.discardBottom(count -> { scenario.discardBottom(count -> {
scenario.assertSizes(7, 33); scenario.assertSizes(7, 33);
assertEquals(1, count); assertEquals(1, count);
assertEquals(hand1, new HashSet<>(scenario.getLibraryRangeSize(26, 7))); assertEquals(hand1, new HashSet<>(scenario.getLibraryRangeSize(33-getCardsPerMull(), 7)));
scenario.getHand().stream().limit(count).forEach(discarded::add); scenario.getHand().stream().limit(count).forEach(discarded::add);
remainingHand.addAll(Sets.difference(scenario.getHand(), new HashSet<>(discarded))); remainingHand.addAll(Sets.difference(scenario.getHand(), new HashSet<>(discarded)));
return discarded; return discarded;
}); });
scenario.mulligan(() -> { scenario.mulligan(() -> {
scenario.assertSizes(6, 34); scenario.assertSizes(6, 34);
assertEquals(hand1, new HashSet<>(scenario.getLibraryRangeSize(26, 7))); assertEquals(hand1, new HashSet<>(scenario.getLibraryRangeSize(33-getCardsPerMull(), 7)));
hand2.addAll(scenario.getHand()); hand2.addAll(scenario.getHand());
return true; return true;
}); });
scenario.discardBottom(count -> { scenario.discardBottom(count -> {
scenario.assertSizes(7, 33); scenario.assertSizes(7, 33);
assertEquals(1, count); assertEquals(1, count);
assertEquals(hand1, new HashSet<>(scenario.getLibraryRangeSize(19, 7))); assertEquals(hand1, new HashSet<>(scenario.getLibraryRangeSize(33-getCardsPerMull()*2, 7)));
assertEquals(discarded, scenario.getLibraryRangeSize(26, 1)); assertEquals(discarded, scenario.getLibraryRangeSize(33-getCardsPerMull(), 1));
assertEquals(hand2, new HashSet<>(scenario.getLibraryRangeSize(27, 6))); assertEquals(hand2, new HashSet<>(scenario.getLibraryRangeSize(34-getCardsPerMull(), 6)));
discarded.clear(); discarded.clear();
remainingHand.clear(); remainingHand.clear();
scenario.getHand().stream().limit(count).forEach(discarded::add); scenario.getHand().stream().limit(count).forEach(discarded::add);
@ -104,9 +109,9 @@ public class LondonMulliganTest extends MulliganTestBase {
scenario.discardBottom(count -> { scenario.discardBottom(count -> {
scenario.assertSizes(6, 34); scenario.assertSizes(6, 34);
assertEquals(1, count); assertEquals(1, count);
assertEquals(hand1, new HashSet<>(scenario.getLibraryRangeSize(19, 7))); assertEquals(hand1, new HashSet<>(scenario.getLibraryRangeSize(33-getCardsPerMull()*2, 7)));
assertEquals(discarded, scenario.getNBottomOfLibrary(1)); assertEquals(discarded, scenario.getNBottomOfLibrary(1));
assertEquals(hand2, new HashSet<>(scenario.getLibraryRangeSize(27, 6))); assertEquals(hand2, new HashSet<>(scenario.getLibraryRangeSize(34-getCardsPerMull(), 6)));
discarded.clear(); discarded.clear();
remainingHand.clear(); remainingHand.clear();
scenario.getHand().stream().limit(count).forEach(discarded::add); scenario.getHand().stream().limit(count).forEach(discarded::add);
@ -115,8 +120,8 @@ public class LondonMulliganTest extends MulliganTestBase {
}); });
scenario.mulligan(() -> { scenario.mulligan(() -> {
scenario.assertSizes(5, 35); scenario.assertSizes(5, 35);
assertEquals(hand1, new HashSet<>(scenario.getLibraryRangeSize(19, 7))); assertEquals(hand1, new HashSet<>(scenario.getLibraryRangeSize(33-getCardsPerMull()*2, 7)));
assertEquals(hand2, new HashSet<>(scenario.getLibraryRangeSize(27, 6))); assertEquals(hand2, new HashSet<>(scenario.getLibraryRangeSize(34-getCardsPerMull(), 6)));
assertEquals(discarded, scenario.getNBottomOfLibrary(1)); assertEquals(discarded, scenario.getNBottomOfLibrary(1));
hand3.addAll(scenario.getHand()); hand3.addAll(scenario.getHand());
return false; return false;
@ -124,8 +129,8 @@ public class LondonMulliganTest extends MulliganTestBase {
scenario.run(() -> { scenario.run(() -> {
scenario.assertSizes(5, 35); scenario.assertSizes(5, 35);
assertEquals(remainingHand, new HashSet<>(scenario.getHand())); assertEquals(remainingHand, new HashSet<>(scenario.getHand()));
assertEquals(hand1, new HashSet<>(scenario.getLibraryRangeSize(19, 7))); assertEquals(hand1, new HashSet<>(scenario.getLibraryRangeSize(33-getCardsPerMull()*2, 7)));
assertEquals(hand2, new HashSet<>(scenario.getLibraryRangeSize(27, 6))); assertEquals(hand2, new HashSet<>(scenario.getLibraryRangeSize(34-getCardsPerMull(), 6)));
assertEquals(hand3, scenario.getHand()); assertEquals(hand3, scenario.getHand());
assertEquals(discarded, scenario.getNBottomOfLibrary(1)); assertEquals(discarded, scenario.getNBottomOfLibrary(1));
}); });
@ -133,7 +138,7 @@ public class LondonMulliganTest extends MulliganTestBase {
@Test @Test
public void testLondonMulligan_FreeMulligan_NoMulligan() { public void testLondonMulligan_FreeMulligan_NoMulligan() {
MulliganScenarioTest scenario = new MulliganScenarioTest(MulliganType.LONDON, 1); MulliganScenarioTest scenario = new MulliganScenarioTest(getMullType(), 1);
Set<UUID> hand1 = new HashSet<>(); Set<UUID> hand1 = new HashSet<>();
scenario.mulligan(() -> { scenario.mulligan(() -> {
scenario.assertSizes(7, 33); scenario.assertSizes(7, 33);
@ -148,7 +153,7 @@ public class LondonMulliganTest extends MulliganTestBase {
@Test @Test
public void testLondonMulligan_FreeMulligan_OneMulligan() { public void testLondonMulligan_FreeMulligan_OneMulligan() {
MulliganScenarioTest scenario = new MulliganScenarioTest(MulliganType.LONDON, 1); MulliganScenarioTest scenario = new MulliganScenarioTest(getMullType(), 1);
Set<UUID> hand1 = new HashSet<>(); Set<UUID> hand1 = new HashSet<>();
Set<UUID> hand2 = new HashSet<>(); Set<UUID> hand2 = new HashSet<>();
scenario.mulligan(() -> { scenario.mulligan(() -> {
@ -159,19 +164,19 @@ public class LondonMulliganTest extends MulliganTestBase {
scenario.mulligan(() -> { scenario.mulligan(() -> {
scenario.assertSizes(7, 33); scenario.assertSizes(7, 33);
hand2.addAll(scenario.getHand()); hand2.addAll(scenario.getHand());
assertEquals(hand1, new HashSet<>(scenario.getLibraryRangeSize(26, 7))); assertEquals(hand1, new HashSet<>(scenario.getLibraryRangeSize(33-getCardsPerMull(), 7)));
return false; return false;
}); });
scenario.run(() -> { scenario.run(() -> {
scenario.assertSizes(7, 33); scenario.assertSizes(7, 33);
assertEquals(hand1, new HashSet<>(scenario.getLibraryRangeSize(26, 7))); assertEquals(hand1, new HashSet<>(scenario.getLibraryRangeSize(33-getCardsPerMull(), 7)));
assertEquals(hand2, new HashSet<>(scenario.getHand())); assertEquals(hand2, new HashSet<>(scenario.getHand()));
}); });
} }
@Test @Test
public void testLondonMulligan_FreeMulligan_TwoMulligan() { public void testLondonMulligan_FreeMulligan_TwoMulligan() {
MulliganScenarioTest scenario = new MulliganScenarioTest(MulliganType.LONDON, 1); MulliganScenarioTest scenario = new MulliganScenarioTest(getMullType(), 1);
Set<UUID> hand1 = new HashSet<>(); Set<UUID> hand1 = new HashSet<>();
Set<UUID> hand2 = new HashSet<>(); Set<UUID> hand2 = new HashSet<>();
Set<UUID> hand3 = new HashSet<>(); Set<UUID> hand3 = new HashSet<>();
@ -184,31 +189,31 @@ public class LondonMulliganTest extends MulliganTestBase {
}); });
scenario.mulligan(() -> { scenario.mulligan(() -> {
scenario.assertSizes(7, 33); scenario.assertSizes(7, 33);
assertEquals(hand1, new HashSet<>(scenario.getLibraryRangeSize(26, 7))); assertEquals(hand1, new HashSet<>(scenario.getLibraryRangeSize(33-getCardsPerMull(), 7)));
hand2.addAll(scenario.getHand()); hand2.addAll(scenario.getHand());
return true; return true;
}); });
scenario.discardBottom(count -> { scenario.discardBottom(count -> {
scenario.assertSizes(7, 33); scenario.assertSizes(7, 33);
assertEquals(1, count); assertEquals(1, count);
assertEquals(hand1, new HashSet<>(scenario.getLibraryRangeSize(19, 7))); assertEquals(hand1, new HashSet<>(scenario.getLibraryRangeSize(33-getCardsPerMull()*2, 7)));
assertEquals(hand2, new HashSet<>(scenario.getLibraryRangeSize(26, 7))); assertEquals(hand2, new HashSet<>(scenario.getLibraryRangeSize(33-getCardsPerMull(), 7)));
scenario.getHand().stream().limit(count).forEach(discarded::add); scenario.getHand().stream().limit(count).forEach(discarded::add);
remainingHand.addAll(Sets.difference(scenario.getHand(), new HashSet<>(discarded))); remainingHand.addAll(Sets.difference(scenario.getHand(), new HashSet<>(discarded)));
return discarded; return discarded;
}); });
scenario.mulligan(() -> { scenario.mulligan(() -> {
scenario.assertSizes(6, 34); scenario.assertSizes(6, 34);
assertEquals(hand1, new HashSet<>(scenario.getLibraryRangeSize(19, 7))); assertEquals(hand1, new HashSet<>(scenario.getLibraryRangeSize(33-getCardsPerMull()*2, 7)));
assertEquals(hand2, new HashSet<>(scenario.getLibraryRangeSize(26, 7))); assertEquals(hand2, new HashSet<>(scenario.getLibraryRangeSize(33-getCardsPerMull(), 7)));
assertEquals(discarded, scenario.getNBottomOfLibrary(1)); assertEquals(discarded, scenario.getNBottomOfLibrary(1));
hand3.addAll(scenario.getHand()); hand3.addAll(scenario.getHand());
return false; return false;
}); });
scenario.run(() -> { scenario.run(() -> {
scenario.assertSizes(6, 34); scenario.assertSizes(6, 34);
assertEquals(hand1, new HashSet<>(scenario.getLibraryRangeSize(19, 7))); assertEquals(hand1, new HashSet<>(scenario.getLibraryRangeSize(33-getCardsPerMull()*2, 7)));
assertEquals(hand2, new HashSet<>(scenario.getLibraryRangeSize(26, 7))); assertEquals(hand2, new HashSet<>(scenario.getLibraryRangeSize(33-getCardsPerMull(), 7)));
assertEquals(hand3, scenario.getHand()); assertEquals(hand3, scenario.getHand());
assertEquals(remainingHand, new HashSet<>(scenario.getHand())); assertEquals(remainingHand, new HashSet<>(scenario.getHand()));
assertEquals(discarded, scenario.getNBottomOfLibrary(1)); assertEquals(discarded, scenario.getNBottomOfLibrary(1));
@ -217,7 +222,7 @@ public class LondonMulliganTest extends MulliganTestBase {
@Test @Test
public void testLondonMulligan_AlwaysMulligan() { public void testLondonMulligan_AlwaysMulligan() {
MulliganScenarioTest scenario = new MulliganScenarioTest(MulliganType.LONDON, 0); MulliganScenarioTest scenario = new MulliganScenarioTest(getMullType(), 0);
scenario.mulligan(() -> { scenario.mulligan(() -> {
scenario.assertSizes(7, 33); scenario.assertSizes(7, 33);
return true; return true;

View file

@ -3,6 +3,7 @@ package org.mage.test.mulligan;
import mage.cards.CardSetInfo; import mage.cards.CardSetInfo;
import mage.cards.basiclands.Forest; import mage.cards.basiclands.Forest;
import mage.cards.decks.Deck; import mage.cards.decks.Deck;
import mage.cards.s.Squire;
import mage.constants.RangeOfInfluence; import mage.constants.RangeOfInfluence;
import mage.game.Game; import mage.game.Game;
import mage.game.GameOptions; import mage.game.GameOptions;
@ -141,7 +142,10 @@ public class MulliganTestBase {
public static Deck generateDeck(UUID playerId, int count) { public static Deck generateDeck(UUID playerId, int count) {
Deck deck = new Deck(); Deck deck = new Deck();
Stream.generate(() -> new Forest(playerId, new CardSetInfo("Forest", "TEST", "1", LAND))) Stream.generate(() -> new Forest(playerId, new CardSetInfo("Forest", "TEST", "1", LAND)))
.limit(count) .limit(count/2+(count & 1)) //If odd number of cards, add one extra forest
.forEach(deck.getCards()::add);
Stream.generate(() -> new Squire(playerId, new CardSetInfo("Squire", "TEST", "2", LAND)))
.limit(count/2)
.forEach(deck.getCards()::add); .forEach(deck.getCards()::add);
return deck; return deck;
} }

View file

@ -0,0 +1,13 @@
package org.mage.test.mulligan;
import mage.game.mulligan.MulliganType;
public class SmoothedLondonMulliganTest extends LondonMulliganTest {
@Override
protected MulliganType getMullType() {
return MulliganType.SMOOTHED_LONDON;
}
@Override
protected int getCardsPerMull() {
return 14;
}
}

View file

@ -1263,7 +1263,7 @@ public abstract class GameImpl implements Game {
player.initLife(this.getStartingLife()); player.initLife(this.getStartingLife());
} }
if (!gameOptions.testMode) { if (!gameOptions.testMode) {
player.drawCards(startingHandSize, null, this); mulligan.drawHand(startingHandSize, player, this);
} }
} }

View file

@ -107,7 +107,7 @@ public class LondonMulligan extends Mulligan {
newHandSize + newHandSize +
(newHandSize == 1 ? " card" : " cards")); (newHandSize == 1 ? " card" : " cards"));
} }
player.drawCards(numCards, null, game); drawHand(numCards, player, game);
while (player.canRespond() && player.getHand().size() > newHandSize) { while (player.canRespond() && player.getHand().size() > newHandSize) {
Target target = new TargetCardInHand(new FilterCard("card (" + (player.getHand().size() - newHandSize) + " more) to put on the bottom of your library")); Target target = new TargetCardInHand(new FilterCard("card (" + (player.getHand().size() - newHandSize) + " more) to put on the bottom of your library"));

View file

@ -91,4 +91,7 @@ public abstract class Mulligan implements Serializable {
return freeMulligans; return freeMulligans;
} }
public void drawHand(int numCards, Player player, Game game){
player.drawCards(numCards, null, game);
}
} }

View file

@ -8,6 +8,7 @@ public enum MulliganType {
VANCOUVER("Vancouver"), VANCOUVER("Vancouver"),
PARIS("Paris"), PARIS("Paris"),
LONDON("London"), LONDON("London"),
SMOOTHED_LONDON("Smoothed London"),
CANADIAN_HIGHLANDER("Canadian Highlander"); CANADIAN_HIGHLANDER("Canadian Highlander");
private final String displayName; private final String displayName;
@ -24,6 +25,8 @@ public enum MulliganType {
return new CanadianHighlanderMulligan(freeMulligans); return new CanadianHighlanderMulligan(freeMulligans);
case VANCOUVER: case VANCOUVER:
return new VancouverMulligan(freeMulligans); return new VancouverMulligan(freeMulligans);
case SMOOTHED_LONDON:
return new SmoothedLondonMulligan(freeMulligans);
default: default:
case LONDON: case LONDON:
return new LondonMulligan(freeMulligans); return new LondonMulligan(freeMulligans);

View file

@ -57,7 +57,7 @@ public class ParisMulligan extends Mulligan {
.append(deduction == 0 ? " for free and draws " : " down to ") .append(deduction == 0 ? " for free and draws " : " down to ")
.append((numCards - deduction)) .append((numCards - deduction))
.append(numCards - deduction == 1 ? " card" : " cards").toString()); .append(numCards - deduction == 1 ? " card" : " cards").toString());
player.drawCards(numCards - deduction, null, game); drawHand(numCards - deduction, player, game);
} }
@Override @Override

View file

@ -0,0 +1,75 @@
package mage.game.mulligan;
import mage.cards.Card;
import mage.cards.CardsImpl;
import mage.cards.ModalDoubleFacedCard;
import mage.game.Game;
import mage.players.Player;
import mage.util.RandomUtil;
import java.util.*;
public class SmoothedLondonMulligan extends LondonMulligan {
public SmoothedLondonMulligan(int freeMulligans) {
super(freeMulligans);
}
SmoothedLondonMulligan(final SmoothedLondonMulligan mulligan) {
super(mulligan);
}
private static double countLands(Collection<Card> cards, boolean library){
double land_count = 0;
for (Card card : cards){
if (card.isLand()) {
land_count += 1;
} else if (card instanceof ModalDoubleFacedCard && ((ModalDoubleFacedCard)card).getRightHalfCard().isLand()){
if (library) { //count MDFCs with a nonland front and a land back as:
land_count += 0.5;//half a land in a library
} else if (RandomUtil.nextBoolean()){
land_count += 1; //randomly as a land or nonland in a hand
// This avoids the bias problem where adjusting the deck land ratio to be (integer vs X.5)/7
// can greatly affect the chance of drawing an MDFC
}
}
}
return land_count;
}
@Override
public void drawHand(int numCards, Player player, Game game){
List<Card> library = player.getLibrary().getCards(game);
if (library.size() >= numCards*2 && numCards > 1) {
double land_ratio = countLands(library, true) / (double) library.size();
Set<Card> hand1 = player.getLibrary().getTopCards(game, numCards);
Set<Card> hand2 = player.getLibrary().getTopCards(game, numCards * 2);
hand2.removeAll(hand1);
double hand1_ratio = countLands(hand1, false) / (double) numCards;
double hand2_ratio = countLands(hand2, false) / (double) numCards;
//distance = max(0,abs(land_ratio-hand_ratio)-0.15)+random()*0.3
//Where land_ratio is (deck lands/deck size) and hand_ratio is (hand lands/hand size)
//Keeps whichever hand's distance is smaller. Note that a 1-land difference is 1/7 = 0.143
//So -0.15 means that there's no change in relative probabilities if within +1/-1 of the expected amount
double hand1_distance = Math.max(0,Math.abs(land_ratio - hand1_ratio)-0.15)+RandomUtil.nextDouble()*0.3;
double hand2_distance = Math.max(0,Math.abs(land_ratio - hand2_ratio)-0.15)+RandomUtil.nextDouble()*0.3;
//game.debugMessage("1: "+hand1_ratio+", 2 = "+hand2_ratio+", expected = "+land_ratio);
//game.debugMessage("hand1: "+hand1_distance+", hand2: "+hand2_distance);
if (hand1_distance < hand2_distance) {
player.drawCards(numCards, null, game);
player.putCardsOnBottomOfLibrary(new CardsImpl(hand2), game, null, false);
//These are immediately shuffled away, but needed for consistent testing
} else {
player.putCardsOnBottomOfLibrary(new CardsImpl(hand1), game, null, false);
player.drawCards(numCards, null, game);
}
player.shuffleLibrary(null, game);
} else { //not enough cards in library or hand, just do a normal draw instead
player.drawCards(numCards, null, game);
}
}
@Override
public SmoothedLondonMulligan copy() {
return new SmoothedLondonMulligan(this);
}
}