nice
This commit is contained in:
385
src/main/java/ee/futur/easygiantsfoundry/HeatActionSolver.java
Normal file
385
src/main/java/ee/futur/easygiantsfoundry/HeatActionSolver.java
Normal file
@@ -0,0 +1,385 @@
|
||||
package ee.futur.easygiantsfoundry;
|
||||
|
||||
//import java.util.ArrayList;
|
||||
//import java.util.List;
|
||||
|
||||
import ee.futur.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 position (dx0), speed (dx1), and acceleration (dx2).
|
||||
* <p>
|
||||
* dx0 - players current heat at tick
|
||||
* dx1 - dx0_current - dx0_last_tick, aka the first derivative
|
||||
* dx2 - dx1_current - dx1_last_tick, aka the second derivative
|
||||
* <p>
|
||||
* 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 = {
|
||||
* 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,
|
||||
* };
|
||||
*/
|
||||
|
||||
/* The following code-snippet can be copy-pasted into runelite developer-tools "Shell" to extract dx1
|
||||
|
||||
import java.awt.Toolkit;
|
||||
import java.awt.datatransfer.StringSelection;
|
||||
import java.awt.datatransfer.Clipboard;
|
||||
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
int HEAT_ID = 13948;
|
||||
|
||||
AtomicInteger tickCounter = new AtomicInteger(-1);
|
||||
AtomicInteger prevHeat = new AtomicInteger(client.getVarbitValue(HEAT_ID));
|
||||
|
||||
|
||||
Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard();
|
||||
|
||||
String output = "";
|
||||
|
||||
subscribe(VarbitChanged.class, ev ->
|
||||
{
|
||||
if (ev.getVarbitId() == HEAT_ID)
|
||||
{
|
||||
int deltaHeat = ev.getValue() - prevHeat.getAndSet(ev.getValue());
|
||||
|
||||
if (deltaHeat == -1) return; // ignore passive drain
|
||||
|
||||
String str = "[" + tickCounter.incrementAndGet()
|
||||
+ "] deltaHeat: " + deltaHeat;
|
||||
log.info(str);
|
||||
output = output + deltaHeat + "\n";
|
||||
StringSelection selection = new StringSelection(output);
|
||||
clipboard.setContents(selection, selection);
|
||||
}
|
||||
});
|
||||
*/
|
||||
|
||||
public class HeatActionSolver
|
||||
{
|
||||
|
||||
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
|
||||
};
|
||||
|
||||
|
||||
// index is stage, ordinal order
|
||||
public static final int[] TOOL_TICK_CYCLE = new int[] {
|
||||
5,
|
||||
2,
|
||||
2
|
||||
};
|
||||
|
||||
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;
|
||||
|
||||
boolean decay = false;
|
||||
|
||||
while (true) {
|
||||
|
||||
if (index > MAX_INDEX)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
if (!overshoot && dx0 + DX_1[index] > goal)
|
||||
{
|
||||
break;
|
||||
}
|
||||
else if (overshoot && dx0 >= goal)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
if (dx0 + DX_1[index] >= max)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
if (decay)
|
||||
{
|
||||
dx0 -= decayValue;
|
||||
}
|
||||
|
||||
|
||||
dx0 += DX_1[index];
|
||||
++index;
|
||||
decay = !decay;
|
||||
}
|
||||
|
||||
if (isFast)
|
||||
{
|
||||
index -= FAST_INDEX;
|
||||
}
|
||||
|
||||
return SolveResult.of(index, dx0, DX_1[index], -1);
|
||||
}
|
||||
|
||||
|
||||
@Value(staticConstructor = "of")
|
||||
public static class DurationResult
|
||||
{
|
||||
int duration;
|
||||
boolean goalInRange;
|
||||
boolean overshooting;
|
||||
int predictedHeat;
|
||||
}
|
||||
|
||||
public static DurationResult solve(
|
||||
Stage stage,
|
||||
int[] range,
|
||||
int actionLeftInStage,
|
||||
int start,
|
||||
boolean isFast,
|
||||
boolean isActionHeating,
|
||||
int paddingTicks,
|
||||
boolean isRunning)
|
||||
{
|
||||
|
||||
final boolean isStageHeating = stage.isHeating();
|
||||
// adding tool cycle ticks because the first cycle at a tool is almost always nulled
|
||||
// (unless manually reaching the tile, then clicking the tool)
|
||||
final int toolDelay = TOOL_TICK_CYCLE[stage.ordinal()];
|
||||
final int travelTicks = solveTravelTicks(isRunning, stage, isActionHeating) + toolDelay;
|
||||
final int travelDecay = (int) Math.ceil((double) travelTicks / 2);
|
||||
|
||||
final int paddingDecay = (int) Math.ceil((double) paddingTicks / 2);
|
||||
|
||||
// adding 2.4s/8ticks worth of padding so preform doesn't decay out of range
|
||||
// average distance from lava+waterfall around 8 ticks
|
||||
// preform decays 1 heat every 2 ticks
|
||||
final int min = Math.max(0, Math.min(1000, range[0] + paddingDecay + travelDecay));
|
||||
final int max = Math.max(0, Math.min(1000, range[1] + paddingDecay + travelDecay));
|
||||
|
||||
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)
|
||||
{
|
||||
int goal = min - actionsLeft_DeltaHeat;
|
||||
goalInRange = goal >= min && goal <= max;
|
||||
|
||||
if (isStageHeating)
|
||||
{
|
||||
|
||||
if (start <= max)
|
||||
{
|
||||
overshoot = !goalInRange;
|
||||
|
||||
if (!goalInRange)
|
||||
{
|
||||
goal = min;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
result = heatingSolve(start, goal, overshoot, max, isFast);
|
||||
|
||||
estimatedDuration = result.index;
|
||||
}
|
||||
}
|
||||
}
|
||||
else // cooling action
|
||||
{
|
||||
int goal = max - actionsLeft_DeltaHeat;
|
||||
goalInRange = goal >= min && goal <= max;
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
private static int solveTravelTicks(boolean isRunning, Stage stage, boolean isLava)
|
||||
{
|
||||
final int distance;
|
||||
if (isLava)
|
||||
{
|
||||
distance = stage.getDistanceToLava();
|
||||
}
|
||||
else
|
||||
{
|
||||
distance = stage.getDistanceToWaterfall();
|
||||
}
|
||||
|
||||
if (isRunning)
|
||||
{
|
||||
// for odd distances, like 7
|
||||
// 7 / 2 = 3.5
|
||||
// rounded to 4
|
||||
return (int) Math.ceil((double) distance / 2);
|
||||
}
|
||||
else
|
||||
{
|
||||
return distance;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user