Optimize mesage generation.
* Use faster stringbuilder for card token formatting instead of full-blown formatter. * When SN/RN/PN and similar placeholders are expanded, calculate the replacement value only if the placeholder is present in the string. In a test with some random games: 55.4M times replaceName was called 39.2M times SN was present 2.0M times PN was present 0.2M times RN was present 0.2M times X was present * Generate event description on demand, instead on event construction. In a test with some random games: 71.601M times description is generated 0.258M time it is actually read or used By postponing generating the description to a point when it is actually needed, CPU time spent when going through possible actions is reduced to about 80%, so in given amount of time, MMAB AI and similar is able to examine larger part of the possible game states.master
parent
d8fcafa0eb
commit
a6e97873fd
|
@ -3,6 +3,8 @@ package magic.model;
|
|||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.Iterator;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.stream.Collectors;
|
||||
import magic.model.choice.MagicCardChoiceResult;
|
||||
import magic.model.phase.MagicPhaseType;
|
||||
|
@ -19,13 +21,13 @@ public class MagicMessage {
|
|||
private final MagicPhaseType phaseType;
|
||||
private final String text;
|
||||
|
||||
MagicMessage(final MagicGame game,final MagicPlayer player,final String text) {
|
||||
this.playerIndex=player.getIndex();
|
||||
this.playerConfig=player.getConfig();
|
||||
this.life=player.getLife();
|
||||
this.turn=game.getTurn();
|
||||
this.phaseType=game.getPhase().getType();
|
||||
this.text=text;
|
||||
MagicMessage(final MagicGame game, final MagicPlayer player, final String text) {
|
||||
this.playerIndex = player.getIndex();
|
||||
this.playerConfig = player.getConfig();
|
||||
this.life = player.getLife();
|
||||
this.turn = game.getTurn();
|
||||
this.phaseType = game.getPhase().getType();
|
||||
this.text = text;
|
||||
}
|
||||
|
||||
public int getPlayerIndex() {
|
||||
|
@ -52,16 +54,16 @@ public class MagicMessage {
|
|||
return text;
|
||||
}
|
||||
|
||||
static void addNames(final StringBuilder builder,final Collection<String> names) {
|
||||
static void addNames(final StringBuilder builder, final Collection<String> names) {
|
||||
if (!names.isEmpty()) {
|
||||
boolean first=true;
|
||||
boolean first = true;
|
||||
boolean next;
|
||||
final Iterator<String> iterator=names.iterator();
|
||||
final Iterator<String> iterator = names.iterator();
|
||||
do {
|
||||
final String name=iterator.next();
|
||||
next=iterator.hasNext();
|
||||
final String name = iterator.next();
|
||||
next = iterator.hasNext();
|
||||
if (first) {
|
||||
first=false;
|
||||
first = false;
|
||||
} else if (next) {
|
||||
builder.append(", ");
|
||||
} else {
|
||||
|
@ -72,12 +74,37 @@ public class MagicMessage {
|
|||
}
|
||||
}
|
||||
|
||||
public static String replaceName(final String sourceText,final Object source, final Object player, final Object ref) {
|
||||
return sourceText
|
||||
.replaceAll("PN", player.toString())
|
||||
.replaceAll("SN", getCardToken(source))
|
||||
.replaceAll("RN", getCardToken(ref))
|
||||
.replaceAll("\\bX\\b" + ARG.EVENQUOTES, getXCost(sourceText, ref));
|
||||
private static Pattern replaceNameRegex = Pattern.compile("PN|SN|RN|\\bX\\b" + ARG.EVENQUOTES);
|
||||
|
||||
public static String replaceName(final String sourceText, final Object source, final Object player, final Object ref) {
|
||||
StringBuffer result = null;
|
||||
Matcher matcher = replaceNameRegex.matcher(sourceText);
|
||||
while (matcher.find()) {
|
||||
if (result == null) result = new StringBuffer(sourceText.length() + 128);
|
||||
String replacement;
|
||||
switch (matcher.group().charAt(0)) {
|
||||
// This relies on the first letter of what could pass through the regex
|
||||
// uniquely identifying the replacement.
|
||||
case 'P': // "PN"
|
||||
replacement = player.toString();
|
||||
break;
|
||||
case 'S': // "SN"
|
||||
replacement = getCardToken(source);
|
||||
break;
|
||||
case 'R': // "RN"
|
||||
replacement = getCardToken(ref);
|
||||
break;
|
||||
case 'X': // "\\bX\\b" + ARG.EVENQUOTES
|
||||
replacement = getXCost(sourceText, ref);
|
||||
break;
|
||||
default:
|
||||
throw new IllegalArgumentException("No replacement for " + matcher.group());
|
||||
}
|
||||
matcher.appendReplacement(result, replacement);
|
||||
}
|
||||
if (result == null) return sourceText;
|
||||
matcher.appendTail(result);
|
||||
return result.toString();
|
||||
}
|
||||
|
||||
public static String replaceChoices(final String sourceText, final Object[] choices) {
|
||||
|
@ -96,11 +123,9 @@ public class MagicMessage {
|
|||
return result;
|
||||
}
|
||||
|
||||
private static final String CARD_TOKEN = "<%s" + CARD_ID_DELIMITER + "%d>";
|
||||
|
||||
private static String getXCost(final String sourceText, final Object obj) {
|
||||
if (obj != null && obj instanceof MagicPayedCost && !sourceText.contains("where X")) {
|
||||
return "X (" + ((MagicPayedCost)obj).getX() + ")";
|
||||
if (obj instanceof MagicPayedCost && !sourceText.contains("where X")) {
|
||||
return "X (" + ((MagicPayedCost) obj).getX() + ")";
|
||||
} else {
|
||||
return "X";
|
||||
}
|
||||
|
@ -119,17 +144,17 @@ public class MagicMessage {
|
|||
|
||||
if (obj instanceof MagicCard) {
|
||||
final MagicCard card = (MagicCard) obj;
|
||||
return String.format(CARD_TOKEN, card.getName(), card.getId());
|
||||
return "<" + card.getName() + CARD_ID_DELIMITER + card.getId() + ">";
|
||||
}
|
||||
|
||||
if (obj instanceof MagicPermanent) {
|
||||
final MagicPermanent card = (MagicPermanent) obj;
|
||||
return String.format(CARD_TOKEN, card.getName(), card.getCard().getId());
|
||||
return "<" + card.getName() + CARD_ID_DELIMITER + card.getCard().getId() + ">";
|
||||
}
|
||||
|
||||
if (obj instanceof MagicCardOnStack) {
|
||||
final MagicCardOnStack card = (MagicCardOnStack) obj;
|
||||
return String.format(CARD_TOKEN, card.getName(), card.getCard().getId());
|
||||
return "<" + card.getName() + CARD_ID_DELIMITER + card.getCard().getId() + ">";
|
||||
}
|
||||
|
||||
if (obj instanceof MagicCardChoiceResult) {
|
||||
|
@ -145,7 +170,7 @@ public class MagicMessage {
|
|||
}
|
||||
|
||||
public static String getCardToken(final String name, final MagicCard card) {
|
||||
return String.format(CARD_TOKEN, name, card.getId());
|
||||
return "<" + name + CARD_ID_DELIMITER + card.getId() + ">";
|
||||
}
|
||||
|
||||
public static String getTokenizedCardNames(final Collection<MagicCard> cards) {
|
||||
|
|
|
@ -52,7 +52,9 @@ public class MagicEvent implements MagicCopyable {
|
|||
private final MagicChoice choice;
|
||||
private final MagicTargetPicker<?> targetPicker;
|
||||
private final MagicEventAction action;
|
||||
private final String description;
|
||||
|
||||
private String generatedDescription;
|
||||
private final String sourceDescription;
|
||||
private final MagicCopyable ref;
|
||||
private boolean isCost = false;
|
||||
|
||||
|
@ -81,7 +83,7 @@ public class MagicEvent implements MagicCopyable {
|
|||
this.targetPicker=targetPicker;
|
||||
this.ref=ref;
|
||||
this.action=action;
|
||||
this.description=MagicMessage.replaceName(description,source,player,ref);
|
||||
this.sourceDescription=description;
|
||||
}
|
||||
|
||||
public MagicEvent(
|
||||
|
@ -266,7 +268,8 @@ public class MagicEvent implements MagicCopyable {
|
|||
targetPicker = sourceEvent.targetPicker;
|
||||
ref = copyMap.copy(sourceEvent.ref);
|
||||
action = sourceEvent.action;
|
||||
description = sourceEvent.description;
|
||||
sourceDescription = sourceEvent.sourceDescription;
|
||||
generatedDescription = sourceEvent.generatedDescription;
|
||||
isCost = sourceEvent.isCost;
|
||||
}
|
||||
|
||||
|
@ -405,7 +408,7 @@ public class MagicEvent implements MagicCopyable {
|
|||
final List<Object[]> choices = choice.getArtificialChoiceResults(game,this);
|
||||
final long time = System.currentTimeMillis() - start;
|
||||
if (time > 1000) {
|
||||
System.err.println("WARNING. ACR: " + choice.getDescription() + description + " time: " + time);
|
||||
System.err.println("WARNING. ACR: " + choice.getDescription() + getDescription() + " time: " + time);
|
||||
/*
|
||||
if (getClass().desiredAssertionStatus()) {
|
||||
throw new GameException("ACR: " + choice.getDescription() + description + " time: " + time, game);
|
||||
|
@ -420,7 +423,7 @@ public class MagicEvent implements MagicCopyable {
|
|||
final Object[] res = choice.getSimulationChoiceResult(game,this);
|
||||
final long time = System.currentTimeMillis() - start;
|
||||
if (time > 1000) {
|
||||
System.err.println("WARNING. RCR: " + choice.getDescription() + description + " time: " + time);
|
||||
System.err.println("WARNING. RCR: " + choice.getDescription() + getDescription() + " time: " + time);
|
||||
/*
|
||||
if (getClass().desiredAssertionStatus()) {
|
||||
throw new GameException("RCR: " + choice.getDescription() + description + " time: " + time, game);
|
||||
|
@ -451,7 +454,7 @@ public class MagicEvent implements MagicCopyable {
|
|||
}
|
||||
|
||||
public final String getDescription(final Object[] choiceResults) {
|
||||
return MagicMessage.replaceChoices(description, choiceResults);
|
||||
return MagicMessage.replaceChoices(getDescription(), choiceResults);
|
||||
}
|
||||
|
||||
public final String getChoiceDescription() {
|
||||
|
@ -750,7 +753,7 @@ public class MagicEvent implements MagicCopyable {
|
|||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "EVENT: " + source + " " + description + " " + (hasChoice() ? choice.getDescription() : "");
|
||||
return "EVENT: " + source + " " + getDescription() + " " + (hasChoice() ? choice.getDescription() : "");
|
||||
}
|
||||
|
||||
public long getStateId() {
|
||||
|
@ -761,7 +764,9 @@ public class MagicEvent implements MagicCopyable {
|
|||
choice.getStateId(),
|
||||
targetPicker.hashCode(),
|
||||
action.hashCode(),
|
||||
description.hashCode(),
|
||||
// Generated description depends on source, player and ref (all those are input to the hash)
|
||||
// and sourceDescription. We can just use sourceDescription directly
|
||||
sourceDescription.hashCode(),
|
||||
MagicObjectImpl.getStateId(ref),
|
||||
isCost ? 1L : -1L,
|
||||
});
|
||||
|
@ -772,7 +777,10 @@ public class MagicEvent implements MagicCopyable {
|
|||
}
|
||||
|
||||
public String getDescription() {
|
||||
return description;
|
||||
if (generatedDescription == null) {
|
||||
generatedDescription = MagicMessage.replaceName(sourceDescription, source, player, ref);
|
||||
}
|
||||
return generatedDescription;
|
||||
}
|
||||
|
||||
public void setCost() {
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
package magic.model.mstatic;
|
||||
package magic.model;
|
||||
|
||||
import magic.model.MagicManaCost;
|
||||
import org.junit.Test;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
|
@ -31,7 +30,7 @@ public class MagicManaCostTest {
|
|||
assertEquals("{X}{X}{U}", uxx.reducedBy(MagicManaCost.create("{2}")).toString());
|
||||
|
||||
assertEquals("{X}{R}", rx.increasedBy(MagicManaCost.create("{2}")).reducedBy(
|
||||
MagicManaCost.create("{2}")).toString());
|
||||
MagicManaCost.create("{2}")).toString());
|
||||
}
|
||||
|
||||
@Test
|
|
@ -0,0 +1,40 @@
|
|||
package magic.model;
|
||||
|
||||
import magic.ai.MagicAIImpl;
|
||||
import magic.model.player.AiProfile;
|
||||
import magic.model.player.HumanProfile;
|
||||
import org.junit.Test;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
|
||||
public class MagicMessageTest {
|
||||
private DuelPlayerConfig cfgAi = new DuelPlayerConfig(AiProfile.create(MagicAIImpl.MMAB, 12), new MagicDeckProfile("r"));
|
||||
private DuelPlayerConfig cfg = new DuelPlayerConfig(HumanProfile.create("myself"), new MagicDeckProfile("r"));
|
||||
private MagicPlayer player = new MagicPlayer(20, cfg, 0);
|
||||
private MagicPlayer playerAi = new MagicPlayer(20, cfgAi, 0);
|
||||
|
||||
@Test
|
||||
public void testNameReplacement() {
|
||||
MagicCardDefinition def = new MagicCardDefinition();
|
||||
def.setName("Card Name");
|
||||
MagicCardDefinition defRef = new MagicCardDefinition();
|
||||
defRef.setName("Recard Name");
|
||||
MagicCard source = new MagicCard(def, player, 1);
|
||||
MagicCard ref = new MagicCard(defRef, player, 2);
|
||||
MagicInteger numRef = new MagicInteger(7);
|
||||
assertEquals("Sacrifice <Card Name~1>",
|
||||
MagicMessage.replaceName("Sacrifice SN", source, player, ref));
|
||||
assertEquals("myself draws 7 cards",
|
||||
MagicMessage.replaceName("PN draws RN cards", source, player, numRef));
|
||||
assertEquals("myself deals 1 damage to <Recard Name~2>",
|
||||
MagicMessage.replaceName("PN deals 1 damage to RN", source, player, ref));
|
||||
assertEquals("Pay 7 life",
|
||||
MagicMessage.replaceName("Pay RN life", source, player, numRef));
|
||||
assertEquals("MMAB has no cards to discard",
|
||||
MagicMessage.replaceName("PN has no cards to discard", source, playerAi, ref));
|
||||
assertEquals("Target player$ draws X cards, where X is the number of lands.",
|
||||
MagicMessage.replaceName("Target player$ draws X cards, where X is the number of lands.", source, player, ref));
|
||||
assertEquals("Pay {X}{G}$",
|
||||
MagicMessage.replaceName("Pay {X}{G}$", source, playerAi, ref));
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue