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

@ -1263,7 +1263,7 @@ public abstract class GameImpl implements Game {
player.initLife(this.getStartingLife());
}
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 == 1 ? " card" : " cards"));
}
player.drawCards(numCards, null, game);
drawHand(numCards, player, game);
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"));

View file

@ -91,4 +91,7 @@ public abstract class Mulligan implements Serializable {
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"),
PARIS("Paris"),
LONDON("London"),
SMOOTHED_LONDON("Smoothed London"),
CANADIAN_HIGHLANDER("Canadian Highlander");
private final String displayName;
@ -24,6 +25,8 @@ public enum MulliganType {
return new CanadianHighlanderMulligan(freeMulligans);
case VANCOUVER:
return new VancouverMulligan(freeMulligans);
case SMOOTHED_LONDON:
return new SmoothedLondonMulligan(freeMulligans);
default:
case LONDON:
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((numCards - deduction))
.append(numCards - deduction == 1 ? " card" : " cards").toString());
player.drawCards(numCards - deduction, null, game);
drawHand(numCards - deduction, player, game);
}
@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);
}
}