Skip to content

Commit

Permalink
CrystalBallManager (#332)
Browse files Browse the repository at this point in the history
* CrystalBallManager

* unused urlString parameter
this was there because I initially thought that any request not in adventure.php
or fight.php wouldn't affect predictions

It's worth noting that while that assertion ended up not being true,
we'll most likely end up needing to use it again later.
This is because relying on KoLAdventure.lastLocationName is error prone,
because we only want to list visits to adventure.php locations,
and KoLAdventure.lastLocationName currently includes
locations such as cellar.php.

(I guess we could also try to spot them at the start of the method...)

* move isCrystalBallX methods to CrystalBallManager

* isCrystalBallX methods test

* Collection.forEach doesn't like concurrent modifications

* fight tests

* update test name

Co-authored-by: Samuel Gaus <sam@gaus.co.uk>
  • Loading branch information
fredg1 and gausie committed Dec 14, 2021
1 parent c67cf9a commit b26f260
Show file tree
Hide file tree
Showing 14 changed files with 367 additions and 75 deletions.
3 changes: 1 addition & 2 deletions src/data/defaults.txt
Expand Up @@ -461,8 +461,7 @@ user crimbotChassis
user crimbotArm
user crimbotPropulsion
user crudeMonster
user crystalBallLocation
user crystalBallMonster
user crystalBallPredictions
user csServicesPerformed
user cubelingProgress 0
user currentEasyBountyItem
Expand Down
3 changes: 2 additions & 1 deletion src/net/sourceforge/kolmafia/AreaCombatData.java
Expand Up @@ -23,6 +23,7 @@
import net.sourceforge.kolmafia.preferences.Preferences;
import net.sourceforge.kolmafia.request.FightRequest;
import net.sourceforge.kolmafia.session.BanishManager;
import net.sourceforge.kolmafia.session.CrystalBallManager;
import net.sourceforge.kolmafia.session.EncounterManager;
import net.sourceforge.kolmafia.session.EncounterManager.EncounterType;
import net.sourceforge.kolmafia.session.EquipmentManager;
Expand Down Expand Up @@ -917,7 +918,7 @@ private String getMonsterString(

if (EncounterManager.isSaberForceMonster(name, this.getZone())) {
buffer.append("forced by the saber");
} else if (EncounterManager.isCrystalBallMonster(name, this.getZone())) {
} else if (CrystalBallManager.isCrystalBallMonster(name, this.getZone())) {
buffer.append("predicted by crystal ball");
} else if (weighting == -1) {
buffer.append("ultra-rare");
Expand Down
2 changes: 2 additions & 0 deletions src/net/sourceforge/kolmafia/KoLCharacter.java
Expand Up @@ -56,6 +56,7 @@
import net.sourceforge.kolmafia.session.ChoiceManager;
import net.sourceforge.kolmafia.session.ClanManager;
import net.sourceforge.kolmafia.session.ContactManager;
import net.sourceforge.kolmafia.session.CrystalBallManager;
import net.sourceforge.kolmafia.session.DisplayCaseManager;
import net.sourceforge.kolmafia.session.EquipmentManager;
import net.sourceforge.kolmafia.session.EventManager;
Expand Down Expand Up @@ -374,6 +375,7 @@ public static final void reset(boolean newCharacter) {
MicroBreweryRequest.reset();
HellKitchenRequest.reset();

CrystalBallManager.reset();
DisplayCaseManager.clearCache();
DwarfFactoryRequest.reset();
EquipmentManager.resetEquipment();
Expand Down
Expand Up @@ -22,6 +22,7 @@
import net.sourceforge.kolmafia.RequestLogger;
import net.sourceforge.kolmafia.preferences.Preferences;
import net.sourceforge.kolmafia.request.FightRequest;
import net.sourceforge.kolmafia.session.CrystalBallManager;
import net.sourceforge.kolmafia.session.EncounterManager;
import net.sourceforge.kolmafia.utilities.RollingLinkedList;

Expand Down Expand Up @@ -266,8 +267,8 @@ public static double applyQueueEffects(
return EncounterManager.isSaberForceMonster(monster, zone) ? 100.0 : 0.0;
}

if (EncounterManager.isCrystalBallZone(zone)) {
return EncounterManager.isCrystalBallMonster(monster, zone) ? data.areaCombatPercent() : 0;
if (CrystalBallManager.isCrystalBallZone(zone)) {
return CrystalBallManager.isCrystalBallMonster(monster, zone) ? data.areaCombatPercent() : 0;
}

double denominator = data.totalWeighting();
Expand Down
1 change: 1 addition & 0 deletions src/net/sourceforge/kolmafia/preferences/Preferences.java
Expand Up @@ -173,6 +173,7 @@ public class Preferences {
"crimbotPropulsion",
"crimboTreeDays",
"crudeMonster",
"crystalBallPredictions",
"csServicesPerformed",
"cubelingProgress",
"currentEasyBountyItem",
Expand Down
10 changes: 5 additions & 5 deletions src/net/sourceforge/kolmafia/request/AdventureRequest.java
Expand Up @@ -27,6 +27,7 @@
import net.sourceforge.kolmafia.session.BatManager;
import net.sourceforge.kolmafia.session.ChoiceManager;
import net.sourceforge.kolmafia.session.ConsequenceManager;
import net.sourceforge.kolmafia.session.CrystalBallManager;
import net.sourceforge.kolmafia.session.DvorakManager;
import net.sourceforge.kolmafia.session.EncounterManager;
import net.sourceforge.kolmafia.session.EquipmentManager;
Expand Down Expand Up @@ -398,7 +399,7 @@ public static final String registerEncounter(final GenericRequest request) {
&& !EncounterManager.isDigitizedEncounter(responseText, false)
&& !EncounterManager.isRomanticEncounter(responseText, false)
&& !EncounterManager.isSaberForceMonster()
&& !EncounterManager.isCrystalBallMonster()
&& !CrystalBallManager.isCrystalBallMonster()
&& !FightRequest.edFightInProgress()) {
AdventureQueueDatabase.enqueue(KoLAdventure.lastVisitedLocation(), encounter);
}
Expand All @@ -414,10 +415,9 @@ public static final String registerEncounter(final GenericRequest request) {

TurnCounter.handleTemporaryCounters(type, encounter);

// Spending a turn somewhere should wipe the crystal ball monster prediction.
// Parsing a new prediction just needs to happen *after* this is called
Preferences.setString("crystalBallMonster", "");
Preferences.setString("crystalBallLocation", "");
if (!Preferences.getString("crystalBallPredictions").isEmpty()) {
CrystalBallManager.updateCrystalBallPredictions();
}

return encounter;
}
Expand Down
42 changes: 3 additions & 39 deletions src/net/sourceforge/kolmafia/request/FightRequest.java
Expand Up @@ -44,7 +44,6 @@
import net.sourceforge.kolmafia.objectpool.ItemPool;
import net.sourceforge.kolmafia.objectpool.SkillPool;
import net.sourceforge.kolmafia.persistence.AdventureDatabase;
import net.sourceforge.kolmafia.persistence.AdventureQueueDatabase;
import net.sourceforge.kolmafia.persistence.AdventureSpentDatabase;
import net.sourceforge.kolmafia.persistence.BountyDatabase;
import net.sourceforge.kolmafia.persistence.ConsumablesDatabase;
Expand All @@ -64,6 +63,7 @@
import net.sourceforge.kolmafia.session.BatManager;
import net.sourceforge.kolmafia.session.BugbearManager;
import net.sourceforge.kolmafia.session.ClanManager;
import net.sourceforge.kolmafia.session.CrystalBallManager;
import net.sourceforge.kolmafia.session.DadManager;
import net.sourceforge.kolmafia.session.DreadScrollManager;
import net.sourceforge.kolmafia.session.EncounterManager;
Expand Down Expand Up @@ -2656,18 +2656,7 @@ public static final void updateCombatData(
}

if (KoLCharacter.hasEquipped(ItemPool.MINIATURE_CRYSTAL_BALL, EquipmentManager.FAMILIAR)) {
String predictedMonster = parseCrystalBall(responseText);
String zone = KoLAdventure.lastLocationName;

if (predictedMonster == null) {
zone = null;
}

Preferences.setString(
"crystalBallMonster", predictedMonster == null ? "" : predictedMonster);
Preferences.setString("crystalBallLocation", zone == null ? "" : zone);

AdventureQueueDatabase.enqueue(KoLAdventure.lastVisitedLocation(), predictedMonster);
CrystalBallManager.parseCrystalBall(responseText);
}
}

Expand Down Expand Up @@ -2696,7 +2685,7 @@ public static final void updateCombatData(

// Track monster's start-of-combat attack value
if (currentRound == 1) {
FightRequest.startingAttack = monster.getAttack();
FightRequest.startingAttack = monster != null ? monster.getAttack() : 0;
}

// Assume this response does not warrant a refresh
Expand Down Expand Up @@ -4559,31 +4548,6 @@ private static void parseLassoUsage(String responseText) {
}
}

private static final Pattern[] CRYSTAL_BALL_PATTERNS = {
Pattern.compile("your next fight will be against <b>an? (.*?)</b>"),
Pattern.compile("next monster in this (?:zone is going to|area will) be <b>an? (.*?)</b>"),
Pattern.compile("Look out, there's <b>an? (.*?)</b> right around the next corner"),
Pattern.compile("There's a little you fighting a little <b>(.*?)</b>"),
Pattern.compile("How do you feel about fighting <b>an? (.*?)</b>\\? Coz that's"),
Pattern.compile("the next monster in this area will be <b>an? (.*?)</b>"),
Pattern.compile("and see a tiny you fighting a tiny <b>(.*?)</b> in a tiny"),
Pattern.compile("it looks like there's <b>an? (.*?)</b> prowling around"),
Pattern.compile("and see yourself running into <b>an? (.*?)</b> soon"),
Pattern.compile("showing you an image of yourself fighting <b>an? (.*?)</b>"),
Pattern.compile("you're going to run into <b>an? (.*?)</b>"),
};

private static String parseCrystalBall(final String responseText) {
for (Pattern p : CRYSTAL_BALL_PATTERNS) {
Matcher matcher = p.matcher(responseText);
if (matcher.find()) {
return matcher.group(1);
}
}

return null;
}

public static final void parseCombatItems(String responseText) {
// The current round will be zero when the fight is over.
// If you run with the WOWbar, the combat item dropdown will
Expand Down
174 changes: 174 additions & 0 deletions src/net/sourceforge/kolmafia/session/CrystalBallManager.java
@@ -0,0 +1,174 @@
package net.sourceforge.kolmafia.session;

import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import net.sourceforge.kolmafia.KoLAdventure;
import net.sourceforge.kolmafia.KoLCharacter;
import net.sourceforge.kolmafia.MonsterData;
import net.sourceforge.kolmafia.combat.MonsterStatusTracker;
import net.sourceforge.kolmafia.persistence.AdventureQueueDatabase;
import net.sourceforge.kolmafia.preferences.Preferences;

public final class CrystalBallManager {
private static final Pattern[] CRYSTAL_BALL_PATTERNS = {
Pattern.compile("your next fight will be against <b>an? (.*?)</b>"),
Pattern.compile("next monster in this (?:zone is going to|area will) be <b>an? (.*?)</b>"),
Pattern.compile("Look out, there's <b>an? (.*?)</b> right around the next corner"),
Pattern.compile("There's a little you fighting a little <b>(.*?)</b>"),
Pattern.compile("How do you feel about fighting <b>an? (.*?)</b>\\? Coz that's"),
Pattern.compile("the next monster in this area will be <b>an? (.*?)</b>"),
Pattern.compile("and see a tiny you fighting a tiny <b>(.*?)</b> in a tiny"),
Pattern.compile("it looks like there's <b>an? (.*?)</b> prowling around"),
Pattern.compile("and see yourself running into <b>an? (.*?)</b> soon"),
Pattern.compile("showing you an image of yourself fighting <b>an? (.*?)</b>"),
Pattern.compile("if you stick around here you're going to run into <b>an? (.*?)</b>")
};

public static class Prediction implements Comparable<Prediction> {
public final int turnCount;
public final String location;
public final String monster;

private Prediction(final int turnCount, final String location, final String monster) {
this.turnCount = turnCount;
this.location = location;
this.monster = monster;
}

@Override
public int compareTo(final Prediction o) {
if (this.turnCount != o.turnCount) {
return Integer.valueOf(this.turnCount).compareTo(o.turnCount);
}

return this.location.compareTo(o.location);
}

@Override
public String toString() {
return this.turnCount + ":" + this.location + ":" + this.monster;
}
}

public static final Map<String, Prediction> predictions = new HashMap<>();

public static void reset() {
CrystalBallManager.predictions.clear();

String[] predictions = Preferences.getString("crystalBallPredictions").split("\\|");

for (final String prediction : predictions) {
String[] parts = prediction.split(":", 3);

if (parts.length < 3) {
continue;
}

try {
CrystalBallManager.predictions.put(
parts[1], new Prediction(Integer.parseInt(parts[0]), parts[1], parts[2]));
} catch (NumberFormatException e) {
continue;
}
}
}

private static void updatePreference() {
List<String> predictions =
CrystalBallManager.predictions.values().stream()
.sorted()
.map(p -> p.toString())
.collect(Collectors.toList());

Preferences.setString("crystalBallPredictions", String.join("|", predictions));
}

/** Parses an in-combat miniature crystal ball prediction. */
public static void parseCrystalBall(final String responseText) {
String predictedMonster = parseCrystalBallMonster(responseText);

if (predictedMonster == null) {
return;
}

String lastAdventureName = KoLAdventure.lastLocationName;

CrystalBallManager.predictions.put(
lastAdventureName,
new Prediction(KoLCharacter.getCurrentRun(), lastAdventureName, predictedMonster));

updatePreference();

AdventureQueueDatabase.enqueue(KoLAdventure.lastVisitedLocation(), predictedMonster);
}

private static String parseCrystalBallMonster(final String responseText) {
for (Pattern p : CRYSTAL_BALL_PATTERNS) {
Matcher matcher = p.matcher(responseText);
if (matcher.find()) {
return matcher.group(1);
}
}

return null;
}

public static void updateCrystalBallPredictions() {
if (KoLAdventure.lastVisitedLocation() == null) {
return;
}

String lastAdventureName = KoLAdventure.lastLocationName;

final Iterator<Prediction> it = CrystalBallManager.predictions.values().iterator();
while (it.hasNext()) {
Prediction prediction = it.next();

if (!prediction.location.equals(lastAdventureName)
&& prediction.turnCount + 2 <= KoLCharacter.getCurrentRun()) {
it.remove();
}
}

updatePreference();
}

// EncounterManager methods

public static boolean isCrystalBallZone(final String zone) {
for (final Prediction prediction : CrystalBallManager.predictions.values()) {
if (prediction.location.equalsIgnoreCase(zone)) {
return true;
}
}

return false;
}

public static boolean isCrystalBallMonster() {
return CrystalBallManager.isCrystalBallMonster(
MonsterStatusTracker.getLastMonsterName(), Preferences.getString("nextAdventure"));
}

public static boolean isCrystalBallMonster(final MonsterData monster, final String zone) {
return CrystalBallManager.isCrystalBallMonster(monster.getName(), zone);
}

public static boolean isCrystalBallMonster(final String monster, final String zone) {
// There's no message to check for so assume the correct monster in the correct zone is from the
// crystal ball
for (final Prediction prediction : CrystalBallManager.predictions.values()) {
if (prediction.monster.equalsIgnoreCase(monster)
&& prediction.location.equalsIgnoreCase(zone)) {
return true;
}
}

return false;
}
}
20 changes: 0 additions & 20 deletions src/net/sourceforge/kolmafia/session/EncounterManager.java
Expand Up @@ -284,26 +284,6 @@ public static final boolean isRelativityMonster() {
return false;
}

public static final boolean isCrystalBallMonster() {
return isCrystalBallMonster(
MonsterStatusTracker.getLastMonsterName(), Preferences.getString("nextAdventure"));
}

public static final boolean isCrystalBallZone(String zone) {
return zone.equalsIgnoreCase(Preferences.getString("crystalBallLocation"));
}

public static final boolean isCrystalBallMonster(MonsterData monster, String zone) {
return isCrystalBallMonster(monster.getName(), zone);
}

public static final boolean isCrystalBallMonster(String monster, String zone) {
// There's no message to check for so assume the correct monster in the correct zone is from the
// crystal ball
return monster.equalsIgnoreCase(Preferences.getString("crystalBallMonster"))
&& isCrystalBallZone(zone);
}

public static final boolean isGregariousEncounter(
final String responseText, final boolean checkMonster) {
if (responseText.contains("Looks like it's that friend you gregariously made")) {
Expand Down

0 comments on commit b26f260

Please sign in to comment.