heat/cool prediction: continued important algorithm bug fix for prediction. Added configurable padding ticks for afk/inefficiency.

This commit is contained in:
Louis Hong
2024-11-02 17:28:55 -07:00
parent 9d7304a436
commit 319f77af3c
7 changed files with 482 additions and 507 deletions

View File

@@ -502,6 +502,31 @@ public interface EasyGiantsFoundryConfig extends Config
description = "Advanced Settings",
position = 5
)
String generalSettings = "generalSettings";
String advancedSettings = "generalSettings";
@ConfigItem(
keyName = "heatActionBuffer", // renamed to reset player's settings for previous bugged implementation
name = "Lava/Waterfall Padding Ticks",
description = "Units in ticks; buffers more than optimal heat when in lava/waterfall calculations to compensate for heat decay when the player is afk or running/walking slower than optimal.",
position = 0,
section = advancedSettings
)
default int heatActionPadTicks()
{
return 3;
}
@ConfigItem(
keyName = "debugging",
name = "Show Debugging",
description = "Shows debugging visuals used for development",
position = 0,
section = advancedSettings,
warning = "Only used for development."
)
default boolean debugging()
{
return false;
}
}

View File

@@ -290,27 +290,27 @@ public class EasyGiantsFoundryPlugin extends Plugin
// start the HeatActionStateMachine when varbit begins to update in onVarbitChanged()
if (event.getMenuOption().startsWith("Heat-preform"))
{
state.heatingCoolingState.stop();
state.heatingCoolingState.setup(7, 0, "heats");
state.heatActionStateMachine.stop();
state.heatActionStateMachine.setup(false, true, "heats");
}
else if (event.getMenuOption().startsWith("Dunk-preform"))
{
state.heatingCoolingState.stop();
state.heatingCoolingState.setup(27, 2, "dunks");
state.heatActionStateMachine.stop();
state.heatActionStateMachine.setup(true, true, "dunks");
}
else if (event.getMenuOption().startsWith("Cool-preform"))
{
state.heatingCoolingState.stop();
state.heatingCoolingState.setup(-7, 0, "cools");
state.heatActionStateMachine.stop();
state.heatActionStateMachine.setup(false, false, "cools");
}
else if (event.getMenuOption().startsWith("Quench-preform"))
{
state.heatingCoolingState.stop();
state.heatingCoolingState.setup(-27, -2, "quenches");
state.heatActionStateMachine.stop();
state.heatActionStateMachine.setup(true, false, "quenches");
}
else // canceled heating/cooling, stop the heating state-machine
else if (!state.heatActionStateMachine.isIdle()) // canceled heating/cooling, stop the heating state-machine
{
state.heatingCoolingState.stop();
state.heatActionStateMachine.stop();
}
}
@@ -333,8 +333,8 @@ public class EasyGiantsFoundryPlugin extends Plugin
// show mould score on Mould UI Title
Widget mouldParent = client.getWidget(47054850);
Integer mouldScore = state.getMouldScore();
if (mouldParent != null && mouldScore != null)
int mouldScore = state.getMouldScore();
if (mouldParent != null && mouldScore >= 0)
{
Widget title = Objects.requireNonNull(mouldParent.getChild(1));
@@ -362,24 +362,41 @@ public class EasyGiantsFoundryPlugin extends Plugin
state.setMouldScore(-1);
}
// start the heating state-machine when the varbit updates
// if heat varbit updated and the user clicked, start the state-machine
if (event.getVarbitId() == VARBIT_HEAT && state.heatingCoolingState.getActionName() != null)
if (event.getVarbitId() == VARBIT_HEAT)
{
// ignore passive heat decay, one heat per two ticks
if (event.getValue() - previousHeat != -1)
int delta = event.getValue() - previousHeat;
// sign check: num * num > 0 == same sign
if (delta != -1)
{
if (state.heatActionStateMachine.getActionname() != null)
{
// if the state-machine is idle, start it
if (state.heatingCoolingState.isIdle())
if (state.heatActionStateMachine.isIdle())
{
state.heatingCoolingState.start(state, config, state.getHeatAmount());
state.heatActionStateMachine.start(state, config, previousHeat);
}
state.heatActionStateMachine.onTick();
}
state.heatingCoolingState.onTick();
if (config.debugging())
{
client.addChatMessage(ChatMessageType.GAMEMESSAGE, "",
"Heat: <col=FF0000>" + event.getValue() + "</col>" +
"Delta: <col=00FFFF>" + delta + "</col> " +
"Heating Ticks: <col=00FFFF>" + state.heatActionStateMachine.heatingTicks + "</col>" +
" Cooling Ticks: <col=00FFFF>" + state.heatActionStateMachine.coolingTicks + "</col>" +
" Remaining Ticks: <col=00FFFF>" + state.heatActionStateMachine.getRemainingDuration(), "");
}
}
// client.addChatMessage(ChatMessageType.GAMEMESSAGE, "", "Delta: <col=00FFFF>" + delta + "</col> ", "");
previousHeat = event.getValue();
}
}
@Subscribe
protected void onConfigChanged(ConfigChanged configChanged)

View File

@@ -335,5 +335,5 @@ public class EasyGiantsFoundryState
return actions;
}
public HeatActionStateMachine heatingCoolingState = new HeatActionStateMachine();
public HeatActionStateMachine heatActionStateMachine = new HeatActionStateMachine();
}

View File

@@ -32,6 +32,7 @@ public class FoundryOverlay3D extends Overlay
{
private static final int HAND_IN_WIDGET = 49414221;
private static final int CRUCIBLE_CAPACITY = 28;
private final ModelOutlineRenderer modelOutlineRenderer;
GameObject tripHammer;
@@ -132,7 +133,7 @@ public class FoundryOverlay3D extends Overlay
Stage stage = state.getCurrentStage();
GameObject stageObject = getStageObject(stage);
if (stageObject == null)
if (stageObject == null || graphics == null)
{
return null;
}
@@ -151,21 +152,13 @@ public class FoundryOverlay3D extends Overlay
drawObjectOutline(graphics, stageObject, color);
}
if ((stage.getHeat() != heat || !state.heatingCoolingState.isIdle()) && config.highlightWaterAndLava())
// !state.heatingCoolingState.isIdle()
// if the stage heat is already in range, but player still wants to do heat changes
if ((stage.getHeat() != heat || !state.heatActionStateMachine.isIdle()) && config.highlightWaterAndLava())
{
drawHeatChangers(graphics);
}
if (state.heatingCoolingState.isCooling())
{
drawHeatChangerOverlay(graphics, waterfall);
}
if (state.heatingCoolingState.isHeating())
{
drawHeatChangerOverlay(graphics, lavaPool);
}
return null;
}
@@ -195,26 +188,87 @@ public class FoundryOverlay3D extends Overlay
modelOutlineRenderer.drawOutline(stageObject, config.borderThickness(), _color, config.borderFeather());
}
private void drawHeatChangerOverlay(
private void drawHeatChangerPreviewOverlay(
Graphics2D graphics,
GameObject stageObject
GameObject stageObject,
boolean isLava
)
{
if (!config.drawLavaWaterInfoOverlay())
{
return;
}
if (state.heatingCoolingState.isIdle())
{
return;
}
int sign = isLava ? 1 : -1;
int fastVelocity = 27 * sign;
int slowVelocity = 7 * sign;
int fastAccelBonus = 2 * sign;
int slowAccelBonus = 0;
HeatActionSolver.DurationResult fastResult =
HeatActionSolver.solve(
state.getCurrentStage(),
state.getCurrentHeatRange(),
state.getActionsLeftInStage(),
state.getHeatAmount(),
true,
isLava,
config.heatActionPadTicks() * 2
);
final int fastDuration = fastResult.getDuration();
HeatActionSolver.DurationResult slowResult =
HeatActionSolver.solve(
state.getCurrentStage(),
state.getCurrentHeatRange(),
state.getActionsLeftInStage(),
state.getHeatAmount(),
false,
isLava,
config.heatActionPadTicks() * 2
);
final int slowDuration = slowResult.getDuration();
final String fastName = isLava ? "dunks" : "quenches";
final String slowName = isLava ? "heats" : "cools";
String text;
text = String.format("%d %s",
state.heatingCoolingState.getRemainingDuration(),
state.heatingCoolingState.getActionName()
if (config.debugging())
{
text = String.format("%d %s (predicted: %d) or %d %s (predicted: %d) (overshoot: %s goal-in-range: %s)",
fastDuration, fastName, fastResult.getPredictedHeat(), slowDuration, slowName, slowResult.getPredictedHeat(), slowResult.isOvershooting(), fastResult.isGoalInRange());
}
else
{
text = String.format("%d %s or %d %s ",
fastDuration, fastName, slowDuration, slowName);
}
LocalPoint stageLoc = stageObject.getLocalLocation();
stageLoc = new LocalPoint(stageLoc.getX(), stageLoc.getY());
Point pos = Perspective.getCanvasTextLocation(client, graphics, stageLoc, text, 50);
Color color = config.lavaWaterfallColour();
OverlayUtil.renderTextLocation(graphics, pos, text, color);
}
private void drawHeatChangerOverlay(Graphics2D graphics, GameObject stageObject)
{
String text;
if (config.debugging())
{
text = String.format("%d %s (overshoot: %s) [goal-in-range: %s]",
state.heatActionStateMachine.getRemainingDuration(),
state.heatActionStateMachine.getActionname(),
state.heatActionStateMachine.isOverShooting(),
state.heatActionStateMachine.isGoalInRange()
);
}
else
{
text = String.format("%d %s",
state.heatActionStateMachine.getRemainingDuration(),
state.heatActionStateMachine.getActionname()
);
}
LocalPoint stageLoc = stageObject.getLocalLocation();
stageLoc = new LocalPoint(stageLoc.getX(), stageLoc.getY());
@@ -230,11 +284,13 @@ public class FoundryOverlay3D extends Overlay
int change = state.getHeatChangeNeeded();
Shape shape = null;
if (change < 0 || state.heatingCoolingState.isCooling())
boolean isLava = change > 0;
boolean isWaterfall = change < 0;
if (isWaterfall || state.heatActionStateMachine.isCooling())
{
shape = waterfall.getClickbox();
}
else if (change > 0 || state.heatingCoolingState.isHeating())
else if (isLava || state.heatActionStateMachine.isHeating())
{
shape = lavaPool.getClickbox();
}
@@ -254,9 +310,28 @@ public class FoundryOverlay3D extends Overlay
graphics.setColor(new Color(color.getRed(), color.getGreen(), color.getBlue(), 20));
graphics.fill(shape);
}
if (config.drawLavaWaterInfoOverlay())
{
if (state.heatActionStateMachine.isCooling())
{
drawHeatChangerOverlay(graphics, waterfall);
}
else if (isWaterfall)
{
drawHeatChangerPreviewOverlay(graphics, waterfall, false);
}
if (state.heatActionStateMachine.isHeating())
{
drawHeatChangerOverlay(graphics, lavaPool);
}
else if (isLava)
{
drawHeatChangerPreviewOverlay(graphics, lavaPool, true);
}
}
}
static final int CRUCIBLE_CAPACITY = 28;
private void drawCrucibleContent(Graphics2D graphics)
{
@@ -430,6 +505,8 @@ public class FoundryOverlay3D extends Overlay
private void drawActionOverlay(Graphics2D graphics, GameObject gameObject)
{
int actionsLeft = state.getActionsLeftInStage();
int heatLeft = state.getActionsForHeatLevel();
@@ -440,6 +517,10 @@ public class FoundryOverlay3D extends Overlay
LocalPoint textLocation = gameObject.getLocalLocation();
textLocation = new LocalPoint(textLocation.getX(), textLocation.getY());
Point canvasLocation = Perspective.getCanvasTextLocation(client, graphics, textLocation, text, 250);
if (canvasLocation == null)
{
return;
}
OverlayUtil.renderTextLocation(graphics, canvasLocation, text, getHeatColor(actionsLeft, heatLeft));
}
if (config.drawActionLeftOverlay())
@@ -449,6 +530,10 @@ public class FoundryOverlay3D extends Overlay
LocalPoint textLocation = gameObject.getLocalLocation();
textLocation = new LocalPoint(textLocation.getX(), textLocation.getY());
Point canvasLocation = Perspective.getCanvasTextLocation(client, graphics, textLocation, text, 250);
if (canvasLocation == null)
{
return;
}
canvasLocation = new Point(canvasLocation.getX(), canvasLocation.getY() + 10);
OverlayUtil.renderTextLocation(graphics, canvasLocation, text, getHeatColor(actionsLeft, heatLeft));
}

View File

@@ -3,13 +3,16 @@ package com.toofifty.easygiantsfoundry;
//import java.util.ArrayList;
//import java.util.List;
import com.toofifty.easygiantsfoundry.enums.Stage;
import lombok.Value;
/**
* Solves the heating/cooling action and predicts tick duration (index)
* the naming convention is focused on the algorithm rather than in-game terminology for the context.
* <p>
* the dx_n refers to successive derivatives of an ordinary-differential-equations
* https://en.wikipedia.org/wiki/Ordinary_differential_equation
* also known as distance (dx0), speed (dx1), and acceleration (dx2).
* also known as position (dx0), speed (dx1), and acceleration (dx2).
* <p>
* dx0 - players current heat at tick
* dx1 - dx0_current - dx0_last_tick, aka the first derivative
@@ -18,7 +21,17 @@ package com.toofifty.easygiantsfoundry;
* for context, here's what dx1 extracted directly from in-game dunking looks like.
* the purpose of the HeatActionSolver.java is to accurately model this data.
* int[] dx1 = {
* 27,
* 7,
* 8,
* 9,
* 11,
* 13,
* 15,
* 17,
* 19,
* 21,
* 24,
* 27, -- dunk/quench starts here
* 30,
* 33,
* 37,
@@ -77,156 +90,254 @@ subscribe(VarbitChanged.class, ev ->
public class HeatActionSolver
{
/**
* <p><b>Warning:</b> this method prefers overshooting goal. For example, if goal is 957,
* it will return index that reaches >957.</p>
*
* <p>This may be desirable if we're aiming to heat just over range minimum;
* for example if the stage is heating (grind stone),</p>
*
* <p>but undesirable when heating to just below range maximum;
* for example if the stage is cooling (hammer.)</p>
*
* <p>
* Make sure to subtract 1 tick from duration, if so.
* </p>
*
*
*
* @param goal the desired heat destination
* @param dx1_init initial speed of heating/cooling. currently 7 for heat/cool, 27 for dunk/quench.
* @param dx2_offset bonus acceleration. currently, 0 for heat/cool, 2 for dunk/quench.
* @return Index here refers to tick. So an index of 10 means the goal can be reached in 10 ticks.
*/
public static int findDuration(int goal, int dx1_init, int dx2_offset)
public static final int[] DX_1 = new int[]{
7,
8,
9,
11,
13,
15,
17,
19,
21,
24,
27, // -- dunk/quench starts here
30,
33,
37,
41,
45,
49,
53,
57,
62,
67,
72,
77,
83,
89,
95,
91, // last one will always overshoot 1000
};
public static final int MAX_INDEX = DX_1.length;
public static final int FAST_INDEX = 10;
@Value(staticConstructor = "of")
public static class SolveResult
{
int index;
int dx0;
int dx1;
int dx2;
}
private static SolveResult heatingSolve(int start, int goal, boolean overshoot, int max, boolean isFast)
{
return relativeSolve(goal - start, overshoot, max - start, isFast, -1);
}
private static SolveResult coolingSolve(int start, int goal, boolean overshoot, int min, boolean isFast)
{
return relativeSolve(start - goal, overshoot, start - min, isFast, 1);
}
private static SolveResult relativeSolve(int goal, boolean overshoot, int max, boolean isFast, int decayValue)
{
int index = isFast ? FAST_INDEX : 0;
int dx0 = 0;
int dx1 = dx1_init;
int count_index = 0;
for (int dx2 = 1; dx0 <= goal; dx2++)
{ // Start from 1 up to the count inclusive
int repetitions;
if (dx2 == 1)
boolean decay = false;
while (true) {
if (index >= MAX_INDEX)
{
repetitions = 2; // The first number appears twice
break;
}
else if (dx2 % 2 == 0)
if (!overshoot && dx0 + DX_1[index] > goal)
{
repetitions = 6; // Even numbers appear six times
break;
}
else
else if (overshoot && dx0 >= goal)
{
repetitions = 4; // Odd numbers (after 1) appear four times
break;
}
for (int j = 0; j < repetitions && dx0 <= goal; j++)
if (dx0 + DX_1[index] >= max)
{
dx0 += dx1;
dx1 += dx2 + dx2_offset; // Sum the current number 'repetitions' times
count_index += 1;
}
}
return count_index;
break;
}
/**
* We can use the pattern to get the dx2 at a specific index numerically
*
* @param index the index/tick we want to calculate dx2 at
* @return the acceleration of heating/cooling at index/tick
*/
public static int getDx2AtIndex(int index)
if (decay)
{
if (index <= 1) return 1;
dx0 -= decayValue;
}
index -= 2;
// 0 1 2 3 4 5 6 7 8 9
// e,e,e,e,e,e,o,o,o,o
dx0 += DX_1[index];
++index;
decay = !decay;
}
int block = index / 10;
int block_idx = index % 10;
int number = block * 2;
if (block_idx <= 5)
if (isFast)
{
return number + 2;
index -= FAST_INDEX;
}
else
return SolveResult.of(index, dx0, DX_1[index], -1);
}
@Value(staticConstructor = "of")
public static class DurationResult
{
return number + 3;
}
int duration;
boolean goalInRange;
boolean overshooting;
int predictedHeat;
}
/**
* We can use the pattern to get the dx1 at a specific index numerically
*
* @param index the index/tick we want to calculate the speed of heating/cooling
* @param constant the initial speed of heating/cooling.
* @return the speed of heating at index/tick
*/
public static int getDx1AtIndex(int index, int constant)
public static DurationResult solve(
Stage stage,
int[] range,
int actionLeftInStage,
int start,
boolean isFast,
boolean isActionHeating,
int padding)
{
int _dx1 = constant;
for (int i = 0; i < index; ++i)
final boolean isStageHeating = stage.isHeating();
// adding 1.8s/6ticks worth of padding so preform doesn't decay out of range
// average distance from lava+waterfall around 6 ticks
// preform decays 1 heat every 2 ticks
final int min = range[0] + padding;
final int max = range[1] + padding;
final int actionsLeft_DeltaHeat = actionLeftInStage * stage.getHeatChange();
int estimatedDuration = 0;
final boolean goalInRange;
boolean overshoot = false;
SolveResult result = null;
// case actions are all cooling, heating is mirrored version
// goal: in-range // stage: heating
// overshoot goal
// <----------|stop|<---------------- heat
// ------|min|----------goal-------|max|
// stage ---------------->
// goal: out-range // stage: heating
// undershoot min
// ...----------|stop|<--------------------- heat
// -goal---|min|---------------------|max|
// stage ----------------->
// goal: in-range // stage: cooling
// undershoot goal
// <-------------------------|stop|<--------------- heat
// ------|min|----------goal-------|max|
// <---------------- stage
// goal: out-range // stage: cooling
// overshoot max
// <--------------------|stop|<--------------- heat
// --------|min|---------------------|max|----goal
// <---------------- stage
if (isActionHeating)
{
_dx1 += getDx2AtIndex(i);
int goal = min - actionsLeft_DeltaHeat;
goalInRange = goal >= min && goal <= max;
if (isStageHeating)
{
if (start <= max)
{
overshoot = !goalInRange;
if (!goalInRange)
{
goal = min;
}
return _dx1;
result = heatingSolve(start, goal, overshoot, max, isFast);
estimatedDuration = result.index;
}
}
else // cooling stage
{
// actionsLeft_DeltaHeat is negative here
if (start <= max)
{
overshoot = goalInRange;
if (!goalInRange)
{
goal = max;
}
// Methods below are functional, but only used to for debugging & development
result = heatingSolve(start, goal, overshoot, max, isFast);
// public static int getDx0AtIndex(int index, int constant)
// {
// int dx0 = 0;
// int dx1 = getDx1AtIndex(0, constant);
// for (int i = 0; i < index; i++)
// { // Start from 1 up to the count inclusive
// int dx2 = getDx2AtIndex(i);
// dx1 += dx2; // Sum the current number 'repetitions' times
// dx0 += dx1;
// }
// return dx0;
// }
estimatedDuration = result.index;
}
}
}
else // cooling action
{
int goal = max - actionsLeft_DeltaHeat;
goalInRange = goal >= min && goal <= max;
// We iteratively generate dx2 into a list
// public static List<Integer> generateDx2List(int count)
// {
// List<Integer> pattern = new ArrayList<>(); // This will hold our pattern
// for (int n = 1, i = 0; i < count; n++)
// { // Start from 1 up to the count inclusive
// int repetitions;
// if (n == 1)
// {
// repetitions = 2; // The first number appears twice
// } else if (n % 2 == 0)
// {
// repetitions = 6; // Even numbers appear six times
// } else
// {
// repetitions = 4; // Odd numbers (after 1) appear four times
// }
// for (int j = 0; j < repetitions && i < count; j++, i++)
// {
// pattern.add(n); // Append the current number 'repetitions' times
// }
// }
// return pattern;
// }
if (isStageHeating)
{
if (start >= min)
{
overshoot = goalInRange;
if (!goalInRange)
{
goal = min;
}
result = coolingSolve(start, goal, overshoot, min, isFast);
estimatedDuration = result.index;
}
}
else // cooling stage cooling action
{
if (start >= min)
{
overshoot = !goalInRange;
if (!goalInRange)
{
goal = max;
}
result = coolingSolve(start, goal, overshoot, min, isFast);
estimatedDuration = result.index;
}
}
}
int dx0 = result == null ? 0 : result.dx0;
if (!isActionHeating)
{
dx0 *= -1;
}
return DurationResult.of(estimatedDuration, goalInRange, overshoot, start + dx0);
}
// public static int findDx0IndexContinue(int goal, int constant, int init_index)
// {
// int dx0 = getDx0AtIndex(init_index, constant);
// int dx1 = getDx1AtIndex(init_index, constant);
// int count_index = init_index;
// for (; dx0 <= goal; count_index++)
// { // Start from 1 up to the count inclusive
// int dx2 = getDx2AtIndex(count_index);
// dx1 += dx2; // Sum the current number 'repetitions' times
// dx0 += dx1;
// }
// return count_index - init_index;
// }
}

View File

@@ -1,6 +1,5 @@
package com.toofifty.easygiantsfoundry;
import com.toofifty.easygiantsfoundry.enums.Stage;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
@@ -14,46 +13,45 @@ public class HeatActionStateMachine
/**
* Tick counter for heating, -1 means not currently heating.
*/
int HeatingTicks = -1;
int heatingTicks = -1;
/**
* Tick counter for cooling, -1 means not currently cooling.
*/
int CoolingTicks = -1;
int coolingTicks = -1;
/**
* The velocity of the heating/cooling action.
*/
int Velocity;
boolean actionFast;
/**
* The acceleration bonus of the heating/cooling action.
*/
int AccelerationBonus;
boolean actionHeating;
/**
* The starting heat amount of the heating/cooling action.
*/
int StartingHeat;
int startingHeat;
/**
* The estimated tick duration of the heating/cooling action.
*/
int EstimatedDuration;
int estimatedDuration;
/**
* The goal heat amount of the heating/cooling action.
*/
int GoalHeat = 0;
int goalHeat = 0;
// debug
boolean goalInRange;
boolean isOverShooting;
int predictedHeat;
/**
* The last action the player clicked on. Used for ui overlay to display.
* When null, the state-machine will stop() and reset.
*/
String ActionName = null;
String actionname = null;
private EasyGiantsFoundryState State;
private EasyGiantsFoundryConfig Config;
private EasyGiantsFoundryState state;
private EasyGiantsFoundryConfig config;
/**
* Start the state-machine with the given parameters.
@@ -69,21 +67,22 @@ public class HeatActionStateMachine
public void start(EasyGiantsFoundryState state, EasyGiantsFoundryConfig config, int startingHeat)
{
// use Velocity to determine if heating or cooling
if (Velocity > 0)
if (actionHeating)
{
HeatingTicks = 0;
CoolingTicks = -1;
heatingTicks = 0;
coolingTicks = -1;
}
else
{
CoolingTicks = 0;
HeatingTicks = -1;
heatingTicks = -1;
coolingTicks = 0;
}
StartingHeat = startingHeat - Velocity;
State = state;
Config = config;
calculateEstimates();
this.startingHeat = startingHeat;
this.state = state;
this.config = config;
updateEstimates();
}
/**
@@ -95,11 +94,11 @@ public class HeatActionStateMachine
{
if (isHeating())
{
return Math.max(0, EstimatedDuration - HeatingTicks);
return Math.max(0, (estimatedDuration - heatingTicks));
}
else if (isCooling())
{
return Math.max(0, EstimatedDuration - CoolingTicks);
return Math.max(0, (estimatedDuration - coolingTicks));
}
else
{
@@ -111,113 +110,26 @@ public class HeatActionStateMachine
* Core logic. Runs once on {@link HeatActionStateMachine#start} and assumes synchronization with the game.
* Calculate the estimated duration and goal heat amount of the heating/cooling action.
*/
public void calculateEstimates()
public void updateEstimates()
{
// 0: left/min 1: right/max
int[] range = State.getCurrentHeatRange();
int stageMin = range[0];
int stageMax = range[1];
Stage stage = State.getCurrentStage();
int actionsLeft = State.getActionsLeftInStage();
int actionsLeft_DeltaHeat = (actionsLeft+1) * stage.getHeatChange();
if (isHeating())
{
if (stage.isHeating())
{
GoalHeat = Math.max(stageMin, stageMax - actionsLeft_DeltaHeat);
if (StartingHeat < GoalHeat)
{
int duration = HeatActionSolver.findDuration(
GoalHeat - StartingHeat,
Velocity, AccelerationBonus
HeatActionSolver.DurationResult result =
HeatActionSolver.solve(
getState().getCurrentStage(),
getState().getCurrentHeatRange(),
getState().getActionsLeftInStage(),
getStartingHeat(),
actionFast,
isHeating(),
config.heatActionPadTicks() * 2
);
// compensate for heat decay during (1 heat every 2 ticks)
GoalHeat += duration / 2;
goalInRange = result.isGoalInRange();
isOverShooting = result.isOvershooting();
EstimatedDuration = HeatActionSolver.findDuration(
GoalHeat - StartingHeat,
Velocity, AccelerationBonus
);
}
else // overheating
{
EstimatedDuration = 0;
}
}
else // is cooling
{
// actionsLeft_DeltaHeat is negative here
GoalHeat = Math.min(stageMax, stageMin - actionsLeft_DeltaHeat);
if (StartingHeat < GoalHeat)
{
int duration = HeatActionSolver.findDuration(
GoalHeat - StartingHeat,
Velocity, AccelerationBonus
) - 1;
predictedHeat = result.getPredictedHeat();
GoalHeat -= duration / 2;
EstimatedDuration = HeatActionSolver.findDuration(
GoalHeat - StartingHeat,
Velocity, AccelerationBonus
) - 1;
}
else // cold enough
{
EstimatedDuration = 0;
}
}
}
else if (isCooling())
{
if (stage.isHeating()) {
GoalHeat = Math.max(stageMin, stageMax - actionsLeft_DeltaHeat);
if (StartingHeat > GoalHeat)
{
int duration = HeatActionSolver.findDuration(
StartingHeat - GoalHeat,
Math.abs(Velocity), Math.abs(AccelerationBonus)
) - 1;
GoalHeat += duration / 2;
EstimatedDuration = HeatActionSolver.findDuration(
(StartingHeat - GoalHeat),
Math.abs(Velocity), Math.abs(AccelerationBonus)
) - 1;
}
else
{
EstimatedDuration = 0;
}
}
// Heating Stage
else {
GoalHeat = Math.max(stageMax, stageMin + actionsLeft_DeltaHeat);
if (StartingHeat > GoalHeat) // too hot
{
int duration = HeatActionSolver.findDuration(
StartingHeat - GoalHeat,
Math.abs(Velocity), Math.abs(AccelerationBonus)
);
GoalHeat -= duration / 2;
EstimatedDuration = HeatActionSolver.findDuration(
StartingHeat - GoalHeat,
Math.abs(Velocity), Math.abs(AccelerationBonus)
);
}
else // hot enough
{
EstimatedDuration = 0;
}
}
}
estimatedDuration = result.getDuration();
}
/**
@@ -227,11 +139,11 @@ public class HeatActionStateMachine
* @param accelerationBonus the acceleration bonus of the heating/cooling action. Usually 0 for slow, 2 for fast.
* @param actionName the name of the action to display in the ui overlay
*/
public void setup(int velocity, int accelerationBonus, String actionName)
public void setup(boolean isFast, boolean isHeating, String actionName)
{
Velocity = velocity;
AccelerationBonus = accelerationBonus;
ActionName = actionName;
actionFast = isFast;
actionHeating = isHeating;
actionname = actionName;
}
/**
@@ -239,9 +151,9 @@ public class HeatActionStateMachine
*/
public void stop()
{
HeatingTicks = -1;
CoolingTicks = -1;
ActionName = null;
heatingTicks = -1;
coolingTicks = -1;
actionname = null;
}
/**
@@ -251,7 +163,7 @@ public class HeatActionStateMachine
*/
public boolean isHeating()
{
return HeatingTicks >= 0;
return heatingTicks >= 0;
}
/**
@@ -261,7 +173,7 @@ public class HeatActionStateMachine
*/
public boolean isCooling()
{
return CoolingTicks >= 0;
return coolingTicks >= 0;
}
/**
@@ -279,25 +191,30 @@ public class HeatActionStateMachine
*/
public void onTick()
{
if (isIdle()) return;
if (isHeating())
{
HeatingTicks++;
if (HeatingTicks >= EstimatedDuration)
if (heatingTicks >= estimatedDuration)
{
stop();
}
}
if (isCooling())
else
{
CoolingTicks++;
if (CoolingTicks >= EstimatedDuration)
heatingTicks++;
}
}
else if (isCooling())
{
if (coolingTicks >= estimatedDuration)
{
stop();
}
else
{
coolingTicks++;
}
}
// log.info("\nReal Heat: " + State.getHeatAmount()
// + "\nGoal Heat - StartingHeat: " + (GoalHeat - StartingHeat)
// + "\nDuration: " + EstimatedDuration);
}
}

View File

@@ -1,180 +0,0 @@
package com.toofifty.easygiantsfoundry;
import static org.junit.Assert.*;
import org.junit.Test;
import java.util.ArrayList;
import java.util.List;
// playground to test HeatSolver
public class HeatSolverTest
{
// @Test
// public void TestHeatSolver_dx2_Iterative()
// {
// final int[] answer =
// {1, 1, 2, 2, 2, 2, 2, 2, 3, 3, 3, 3, 4, 4, 4, 4, 4, 4, 5, 5, 5, 5, 6, 6};
// List<Integer> produced = HeatActionSolver.generateDx2List(answer.length);
//
// System.err.println("Expected Length: " + answer.length + " Length: " + produced.size());
// // print produced
// for (Integer integer : produced)
// {
// System.err.print(integer + ",");
// }
// System.err.println();
// // compare
// for (int i = 0; i < answer.length; i++)
// {
// assertEquals("Asserting dx2 n=" + i, answer[i], produced.get(i).intValue());
// }
// }
@Test
public void TestHeatSolver_dx2_Numerical()
{
final int[] answer =
{1, 1, 2, 2, 2, 2, 2, 2, 3, 3, 3, 3, 4, 4, 4, 4, 4, 4, 5, 5, 5, 5, 6, 6};
// test getDx2AtIndex
for (int i = 0; i < answer.length; i++)
{
assertEquals("Asserting dx2 n=" + i, answer[i], HeatActionSolver.getDx2AtIndex(i));
}
}
@Test
public void TestHeatSolver_dx1()
{
final int c = 7;
final int[] answer =
// {1,1,2, 2, 2, 2, 2, 3, 3, 3, 3, 3, 4, 4, 4, 4, 4, 5, 5, 5, 5, 5, 6, 6, 6, 6, 6};
{7, 8, 9, 11, 13, 15, 17, 19, 21, 24, 27, 30, 33, 37, 41, 45, 49, 53, 57, 62, 67, 72, 77, 83, 89};
for (int i = 0; i < answer.length; i++)
{
assertEquals("Asserting dx1 n=" + i + " c=" + c + " answer=" + answer[i], answer[i], HeatActionSolver.getDx1AtIndex(i, c));
}
}
@Test
public void TestHeatSolver_dx2_from_groundtruth()
{
// runelite-shell script for retrieving heating/cooling delta.
// copy-paste into developer-tools -> Shell
// ground-truth answer from game
final int[] answer =
{7, 8, 9, 11, 13, 15, 17, 19, 21, 24, 27, 30, 33, 37, 41, 45, 49, 53, 57, 62, 67, 72, 77, 83, 89};
for (int i = 0; i < answer.length - 1; i++)
{
System.err.print(answer[i + 1] - answer[i] + ",");
}
}
@Test
public void TestHeatSolver_Dx0()
{
final int[] answer_dx1 =
{7, 8, 9, 11, 13, 15, 17, 19, 21, 24, 27, 30, 33, 37, 41, 45, 49, 53, 57, 62, 67, 72, 77, 83, 89};
List<Integer> answer_dx0 = new ArrayList<>();
int sum = 0;
for (int i = 0; i < answer_dx1.length; i++)
{
sum += answer_dx1[i];
answer_dx0.add(sum);
}
System.err.println(answer_dx0);
for (int i = 0; i < answer_dx1.length; i++)
{
TestHeatSolver_Dx0_Helper(answer_dx0.get(i), answer_dx0.get(0), i);
}
}
@Test
public void TestHeatSolver_Dx0_Manual()
{
for (int i = 0; i < 50; i++)
{
System.err.println("[" + (350 + i) + "]" + HeatActionSolver.findDuration(350 + i, 7, 0));
}
}
@Test
public void TestHeatSolver_Dx0_2()
{
// 7->1,15->2,24->3,35->4,48->5,63->6,80->7,99->8,120->9,144->10,171->11,201->12,234->13,271->14,312->15,357->16,406->17,459->18,516->19,578->20,645->21,717->22,794->23,877->24,966->25
final int[] answer_dx1 =
{7, 8, 9, 11, 13, 15, 17, 19, 21, 24, 27, 30, 33, 37, 41, 45, 49, 53, 57, 62, 67, 72, 77, 83, 89};
List<Integer> answer_dx0 = new ArrayList<>();
int sum = 0;
for (int i = 0; i < answer_dx1.length; i++)
{
sum += answer_dx1[i];
answer_dx0.add(sum);
}
System.err.println(answer_dx0);
// System.err.println(
// HeatSolver.findDx0IndexContinue(406, 7, 0));
// System.err.println(
// HeatSolver.findDx0IndexContinue(406, 7, 10));
// System.err.println(
// HeatSolver.findDx0IndexContinue(406, 7, 17));
//
// System.err.println(
// HeatSolver.findDx0IndexContinue(1000, 7, 0));
System.err.println(
HeatActionSolver.findDuration(957, 27, 2));
// System.err.println(
// HeatActionSolver.findDuration(1000, 7, 1));
}
public void TestHeatSolver_Dx0_Helper(int dx0, int constant, int answer_index)
{
System.err.print(dx0 + "->" + HeatActionSolver.findDuration(dx0, constant, 0) + ",");
// test calcDx0Index
assertEquals("Asserting dx0 for index " + answer_index,
answer_index, HeatActionSolver.findDuration(dx0, constant, 0));
}
@Test
public void Calc()
{
int[] dx1 = {
27,
30,
33,
37,
41,
45,
49,
53,
57,
62,
67,
72,
77,
83,
89,
95,
91,
};
List<Integer> dx2 = new ArrayList<>();
for (int i = 0; i < dx1.length - 1; i++)
{
dx2.add(dx1[i+1] - dx1[i]);
}
System.err.println(dx2);
}
}