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
Bilbo 2019-12-08 08:03:57 +01:00
parent d8fcafa0eb
commit a6e97873fd
4 changed files with 111 additions and 39 deletions

View File

@ -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) {

View File

@ -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() {

View File

@ -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

View File

@ -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));
}
}