HotColdClue: Add hot-cold solver class

This adds a general hot-cold puzzle solver class and implements it in
HotColdClue.
This commit is contained in:
Jordan Atwood
2019-06-12 23:02:25 -07:00
committed by Adam
parent 5a6b39036d
commit e5d4e7a897
4 changed files with 457 additions and 111 deletions

View File

@@ -25,20 +25,15 @@
*/
package net.runelite.client.plugins.cluescrolls.clues;
import com.google.common.collect.Lists;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics2D;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.geom.Rectangle2D;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Collection;
import java.util.EnumMap;
import java.util.Map;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.util.Set;
import java.util.stream.Collectors;
import lombok.Getter;
import net.runelite.api.NPC;
import net.runelite.api.coords.LocalPoint;
@@ -48,6 +43,7 @@ import net.runelite.client.plugins.cluescrolls.ClueScrollPlugin;
import static net.runelite.client.plugins.cluescrolls.ClueScrollWorldOverlay.IMAGE_Z_OFFSET;
import net.runelite.client.plugins.cluescrolls.clues.hotcold.HotColdArea;
import net.runelite.client.plugins.cluescrolls.clues.hotcold.HotColdLocation;
import net.runelite.client.plugins.cluescrolls.clues.hotcold.HotColdSolver;
import net.runelite.client.plugins.cluescrolls.clues.hotcold.HotColdTemperature;
import net.runelite.client.plugins.cluescrolls.clues.hotcold.HotColdTemperatureChange;
import net.runelite.client.ui.overlay.OverlayUtil;
@@ -58,18 +54,17 @@ import net.runelite.client.ui.overlay.components.TitleComponent;
@Getter
public class HotColdClue extends ClueScroll implements LocationClueScroll, LocationsClueScroll, TextClueScroll, NpcClueScroll
{
private static final int HOT_COLD_PANEL_WIDTH = 200;
private static final HotColdClue CLUE =
new HotColdClue("Buried beneath the ground, who knows where it's found. Lucky for you, A man called Jorral may have a clue.",
"Jorral",
"Speak to Jorral to receive a strange device.");
// list of potential places to dig
private List<HotColdLocation> digLocations = new ArrayList<>();
private final String text;
private final String npc;
private final String solution;
private HotColdSolver hotColdSolver;
private WorldPoint location;
private WorldPoint lastWorldPoint;
public static HotColdClue forText(String text)
{
@@ -87,12 +82,13 @@ public class HotColdClue extends ClueScroll implements LocationClueScroll, Locat
this.npc = npc;
this.solution = solution;
setRequiresSpade(true);
initializeSolver();
}
@Override
public WorldPoint[] getLocations()
{
return Lists.transform(digLocations, HotColdLocation::getWorldPoint).toArray(new WorldPoint[0]);
return hotColdSolver.getPossibleLocations().stream().map(HotColdLocation::getWorldPoint).toArray(WorldPoint[]::new);
}
@Override
@@ -101,10 +97,10 @@ public class HotColdClue extends ClueScroll implements LocationClueScroll, Locat
panelComponent.getChildren().add(TitleComponent.builder()
.text("Hot/Cold Clue")
.build());
panelComponent.setPreferredSize(new Dimension(200, 0));
panelComponent.setPreferredSize(new Dimension(HOT_COLD_PANEL_WIDTH, 0));
// strange device has not been tested yet, show how to get it
if (lastWorldPoint == null && location == null)
if (hotColdSolver.getLastWorldPoint() == null && location == null)
{
if (getNpc() != null)
{
@@ -131,7 +127,9 @@ public class HotColdClue extends ClueScroll implements LocationClueScroll, Locat
panelComponent.getChildren().add(LineComponent.builder()
.left("Possible areas:")
.build());
Map<HotColdArea, Integer> locationCounts = new HashMap<>();
final Map<HotColdArea, Integer> locationCounts = new EnumMap<>(HotColdArea.class);
final Collection<HotColdLocation> digLocations = hotColdSolver.getPossibleLocations();
for (HotColdLocation hotColdLocation : digLocations)
{
@@ -159,17 +157,16 @@ public class HotColdClue extends ClueScroll implements LocationClueScroll, Locat
}
else
{
for (HotColdArea s : locationCounts.keySet())
for (HotColdArea area : locationCounts.keySet())
{
panelComponent.getChildren().add(LineComponent.builder()
.left(s.getName() + ":")
.left(area.getName() + ':')
.build());
for (HotColdLocation hotColdLocation : digLocations)
{
if (hotColdLocation.getHotColdArea() == s)
if (hotColdLocation.getHotColdArea() == area)
{
Rectangle2D r = hotColdLocation.getRect();
panelComponent.getChildren().add(LineComponent.builder()
.left("- " + hotColdLocation.getArea())
.leftColor(Color.LIGHT_GRAY)
@@ -185,7 +182,7 @@ public class HotColdClue extends ClueScroll implements LocationClueScroll, Locat
public void makeWorldOverlayHint(Graphics2D graphics, ClueScrollPlugin plugin)
{
// when final location has been found
if (this.location != null)
if (location != null)
{
LocalPoint localLocation = LocalPoint.fromWorld(plugin.getClient(), getLocation());
@@ -198,19 +195,16 @@ public class HotColdClue extends ClueScroll implements LocationClueScroll, Locat
}
// when strange device hasn't been activated yet, show Jorral
if (lastWorldPoint == null)
if (hotColdSolver.getLastWorldPoint() == null && plugin.getNpcsToMark() != null)
{
// Mark NPC
if (plugin.getNpcsToMark() != null)
for (NPC npcToMark : plugin.getNpcsToMark())
{
for (NPC npc : plugin.getNpcsToMark())
{
OverlayUtil.renderActorOverlayImage(graphics, npc, plugin.getClueScrollImage(), Color.ORANGE, IMAGE_Z_OFFSET);
}
OverlayUtil.renderActorOverlayImage(graphics, npcToMark, plugin.getClueScrollImage(), Color.ORANGE, IMAGE_Z_OFFSET);
}
}
// once the number of possible dig locations is below 10, show the dig spots
final Collection<HotColdLocation> digLocations = hotColdSolver.getPossibleLocations();
if (digLocations.size() < 10)
{
// Mark potential dig locations
@@ -251,8 +245,10 @@ public class HotColdClue extends ClueScroll implements LocationClueScroll, Locat
}
else
{
location = null;
final HotColdTemperatureChange temperatureChange = HotColdTemperatureChange.of(message);
updatePossibleArea(localWorld, temperature, temperatureChange);
hotColdSolver.signal(localWorld, temperature, temperatureChange);
}
return true;
@@ -261,88 +257,14 @@ public class HotColdClue extends ClueScroll implements LocationClueScroll, Locat
@Override
public void reset()
{
this.lastWorldPoint = null;
digLocations.clear();
initializeSolver();
}
private void updatePossibleArea(@Nonnull final WorldPoint worldPoint, @Nonnull final HotColdTemperature temperature, @Nullable final HotColdTemperatureChange temperatureChange)
private void initializeSolver()
{
this.location = null;
if (digLocations.isEmpty())
{
digLocations.addAll(Arrays.asList(HotColdLocation.values()));
}
// when the strange device reads a temperature, that means that the center of the final dig location
// is a range of squares away from the player's current location (Chebyshev AKA Chess-board distance)
int maxSquaresAway = temperature.getMaxDistance();
int minSquaresAway = temperature.getMinDistance();
// rectangle r1 encompasses all of the points that are within the max possible distance from the player
Point p1 = new Point(worldPoint.getX() - maxSquaresAway, worldPoint.getY() - maxSquaresAway);
Rectangle r1 = new Rectangle((int) p1.getX(), (int) p1.getY(), 2 * maxSquaresAway + 1, 2 * maxSquaresAway + 1);
// rectangle r2 encompasses all of the points that are within the min possible distance from the player
Point p2 = new Point(worldPoint.getX() - minSquaresAway, worldPoint.getY() - minSquaresAway);
Rectangle r2 = new Rectangle((int) p2.getX(), (int) p2.getY(), 2 * minSquaresAway + 1, 2 * minSquaresAway + 1);
// eliminate from consideration dig spots that lie entirely within the min range or entirely outside of the max range
digLocations.removeIf(entry -> r2.contains(entry.getRect()) || !r1.intersects(entry.getRect()));
// if a previous world point has been recorded, we can consider the warmer/colder result from the strange device
if (lastWorldPoint != null && temperatureChange != null)
{
switch (temperatureChange)
{
case COLDER:
// eliminate spots that are absolutely warmer
digLocations.removeIf(entry -> isFirstPointCloserRect(worldPoint, lastWorldPoint, entry.getRect()));
break;
case WARMER:
// eliminate spots that are absolutely colder
digLocations.removeIf(entry -> isFirstPointCloserRect(lastWorldPoint, worldPoint, entry.getRect()));
break;
case SAME:
// I couldn't figure out a clean implementation for this case
// not necessary for quickly determining final location
}
}
lastWorldPoint = worldPoint;
}
private boolean isFirstPointCloserRect(WorldPoint firstWp, WorldPoint secondWp, Rectangle2D r)
{
WorldPoint p1 = new WorldPoint((int) r.getMaxX(), (int) r.getMaxY(), 0);
if (!isFirstPointCloser(firstWp, secondWp, p1))
{
return false;
}
WorldPoint p2 = new WorldPoint((int) r.getMaxX(), (int) r.getMinY(), 0);
if (!isFirstPointCloser(firstWp, secondWp, p2))
{
return false;
}
WorldPoint p3 = new WorldPoint((int) r.getMinX(), (int)r.getMaxY(), 0);
if (!isFirstPointCloser(firstWp, secondWp, p3))
{
return false;
}
WorldPoint p4 = new WorldPoint((int) r.getMinX(), (int) r.getMinY(), 0);
return (isFirstPointCloser(firstWp, secondWp, p4));
}
private boolean isFirstPointCloser(WorldPoint firstWp, WorldPoint secondWp, WorldPoint wp)
{
int firstDistance = firstWp.distanceTo2D(wp);
int secondDistance = secondWp.distanceTo2D(wp);
return (firstDistance < secondDistance);
final Set<HotColdLocation> locations = Arrays.stream(HotColdLocation.values())
.collect(Collectors.toSet());
hotColdSolver = new HotColdSolver(locations);
}
private void markFinalSpot(WorldPoint wp)
@@ -355,4 +277,4 @@ public class HotColdClue extends ClueScroll implements LocationClueScroll, Locat
{
return new String[] {npc};
}
}
}

View File

@@ -26,7 +26,6 @@
package net.runelite.client.plugins.cluescrolls.clues.hotcold;
import java.awt.Rectangle;
import java.awt.geom.Rectangle2D;
import lombok.AllArgsConstructor;
import lombok.Getter;
import net.runelite.api.coords.WorldPoint;
@@ -180,7 +179,7 @@ public enum HotColdLocation
private final HotColdArea hotColdArea;
private final String area;
public Rectangle2D getRect()
public Rectangle getRect()
{
return new Rectangle(worldPoint.getX() - 4, worldPoint.getY() - 4, 9, 9);
}

View File

@@ -0,0 +1,167 @@
/*
* Copyright (c) 2018, Eadgars Ruse <https://github.com/Eadgars-Ruse>
* Copyright (c) 2019, Jordan Atwood <nightfirecat@protonmail.com>
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
* ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package net.runelite.client.plugins.cluescrolls.clues.hotcold;
import com.google.common.annotations.VisibleForTesting;
import java.awt.Rectangle;
import java.util.Set;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import lombok.Getter;
import net.runelite.api.coords.WorldPoint;
/**
* Solution finder for hot-cold style puzzles.
* <p>
* These puzzles are established by having some way to test the distance from the solution via "warmth", where being
* colder means one is farther away from the target, and being warmer means one is closer to it, with the goal being to
* reach the most warm value to discover the solution point. Hot-cold puzzles in Old School Runescape are implemented
* with specific set of solution points, so this solver will filter from a provided set of possible solutions as new
* signals of temperatures and temperature changes are provided.
*/
@Getter
public class HotColdSolver
{
private final Set<HotColdLocation> possibleLocations;
@Nullable
private WorldPoint lastWorldPoint;
public HotColdSolver(Set<HotColdLocation> possibleLocations)
{
this.possibleLocations = possibleLocations;
}
/**
* Process a hot-cold update given a {@link WorldPoint} where a check occurred and the resulting temperature and
* temperature change discovered at that point. This will filter the set of possible locations which can be the
* solution.
*
* @param worldPoint The point where a hot-cold check occurred
* @param temperature The temperature of the checked point
* @param temperatureChange The change of temperature of the checked point compared to the previously-checked point
* @return A set of {@link HotColdLocation}s which are still possible after the filtering occurs. This return value
* is the same as would be returned by {@code getPossibleLocations()}.
*/
public Set<HotColdLocation> signal(@Nonnull final WorldPoint worldPoint, @Nonnull final HotColdTemperature temperature, @Nullable final HotColdTemperatureChange temperatureChange)
{
// when the strange device reads a temperature, that means that the center of the final dig location
// is a range of squares away from the player's current location (Chebyshev AKA Chess-board distance)
int maxSquaresAway = temperature.getMaxDistance();
int minSquaresAway = temperature.getMinDistance();
// maxDistanceArea encompasses all of the points that are within the max possible distance from the player
final Rectangle maxDistanceArea = new Rectangle(
worldPoint.getX() - maxSquaresAway,
worldPoint.getY() - maxSquaresAway,
2 * maxSquaresAway + 1,
2 * maxSquaresAway + 1);
// minDistanceArea encompasses all of the points that are within the min possible distance from the player
final Rectangle minDistanceArea = new Rectangle(
worldPoint.getX() - minSquaresAway,
worldPoint.getY() - minSquaresAway,
2 * minSquaresAway + 1,
2 * minSquaresAway + 1);
// eliminate from consideration dig spots that lie entirely within the min range or entirely outside of the max range
possibleLocations.removeIf(entry -> minDistanceArea.contains(entry.getRect()) || !maxDistanceArea.intersects(entry.getRect()));
// if a previous world point has been recorded, we can consider the warmer/colder result from the strange device
if (lastWorldPoint != null && temperatureChange != null)
{
switch (temperatureChange)
{
case COLDER:
// eliminate spots that are absolutely warmer
possibleLocations.removeIf(entry -> isFirstPointCloserRect(worldPoint, lastWorldPoint, entry.getRect()));
break;
case WARMER:
// eliminate spots that are absolutely colder
possibleLocations.removeIf(entry -> isFirstPointCloserRect(lastWorldPoint, worldPoint, entry.getRect()));
break;
case SAME:
// I couldn't figure out a clean implementation for this case
// not necessary for quickly determining final location
}
}
lastWorldPoint = worldPoint;
return getPossibleLocations();
}
/**
* Determines whether the first point passed is closer to each corner of the given rectangle than the second point.
*
* @param firstPoint First point to test. Return result will be relating to this point's location.
* @param secondPoint Second point to test
* @param rect Rectangle, whose corner points will be compared to the first and second points passed
* @return {@code true} if {@code firstPoint} is closer to each of {@code rect}'s four corner points than
* {@code secondPoint}, {@code false} otherwise.
* @see WorldPoint#distanceTo2D
*/
@VisibleForTesting
static boolean isFirstPointCloserRect(final WorldPoint firstPoint, final WorldPoint secondPoint, final Rectangle rect)
{
final WorldPoint nePoint = new WorldPoint((rect.x + rect.width), (rect.y + rect.height), 0);
if (!isFirstPointCloser(firstPoint, secondPoint, nePoint))
{
return false;
}
final WorldPoint sePoint = new WorldPoint((rect.x + rect.width), rect.y, 0);
if (!isFirstPointCloser(firstPoint, secondPoint, sePoint))
{
return false;
}
final WorldPoint nwPoint = new WorldPoint(rect.x, (rect.y + rect.height), 0);
if (!isFirstPointCloser(firstPoint, secondPoint, nwPoint))
{
return false;
}
final WorldPoint swPoint = new WorldPoint(rect.x, rect.y, 0);
return (isFirstPointCloser(firstPoint, secondPoint, swPoint));
}
/**
* Determines whether the first point passed is closer to the given point of comparison than the second point.
*
* @param firstPoint First point to test. Return result will be relating to this point's location.
* @param secondPoint Second point to test
* @param worldPoint Point to compare to the first and second points passed
* @return {@code true} if {@code firstPoint} is closer to {@code worldPoint} than {@code secondPoint},
* {@code false} otherwise.
* @see WorldPoint#distanceTo2D
*/
@VisibleForTesting
static boolean isFirstPointCloser(final WorldPoint firstPoint, final WorldPoint secondPoint, final WorldPoint worldPoint)
{
return firstPoint.distanceTo2D(worldPoint) < secondPoint.distanceTo2D(worldPoint);
}
}

View File

@@ -0,0 +1,258 @@
/*
* Copyright (c) 2019, Jordan Atwood <nightfirecat@protonmail.com>
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
* ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package net.runelite.client.plugins.cluescrolls.clues.hotcold;
import com.google.common.collect.Sets;
import java.awt.Rectangle;
import java.util.Set;
import java.util.stream.Collectors;
import static junit.framework.TestCase.assertTrue;
import net.runelite.api.coords.WorldPoint;
import static net.runelite.client.plugins.cluescrolls.clues.hotcold.HotColdSolver.isFirstPointCloser;
import static net.runelite.client.plugins.cluescrolls.clues.hotcold.HotColdSolver.isFirstPointCloserRect;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import org.junit.Test;
public class HotColdSolverTest
{
private static final String RESPONSE_TEXT_ICE_COLD_COLDER = "The device is ice cold, but colder than last time.";
private static final String RESPONSE_TEXT_VERY_COLD_WARMER = "The device is very cold, and warmer than last time.";
private static final String RESPONSE_TEXT_COLD = "The device is cold.";
private static final String RESPONSE_TEXT_COLD_COLDER = "The device is cold, but colder than last time.";
private static final String RESPONSE_TEXT_COLD_WARMER = "The device is cold, and warmer than last time.";
private static final String RESPONSE_TEXT_COLD_SAME_TEMP = "The device is cold, and the same temperature as last time.";
private static final String RESPONSE_TEXT_VERY_HOT = "The device is very hot.";
private static final String RESPONSE_TEXT_VERY_HOT_COLDER = "The device is very hot, but colder than last time.";
private static final String RESPONSE_TEXT_VERY_HOT_WARMER = "The device is very hot, and warmer than last time.";
private static final String RESPONSE_TEXT_VERY_HOT_SAME_TEMP = "The device is very hot, and the same temperature as last time.";
@Test
public void testOneStepSolution()
{
final Set<HotColdLocation> foundLocation = Sets.immutableEnumSet(HotColdLocation.KARAMJA_KHARAZI_NE);
testSolver(createHotColdSolver(), new WorldPoint(2852, 2992, 0), RESPONSE_TEXT_VERY_HOT, foundLocation);
}
@Test
public void testIgnoreStartingTemperatureDifference()
{
final WorldPoint testedPoint = new WorldPoint(2852, 2992, 0);
final Set<HotColdLocation> foundLocations = Sets.immutableEnumSet(HotColdLocation.KARAMJA_KHARAZI_NE);
testSolver(createHotColdSolver(), testedPoint, RESPONSE_TEXT_VERY_HOT, foundLocations);
testSolver(createHotColdSolver(), testedPoint, RESPONSE_TEXT_VERY_HOT_COLDER, foundLocations);
testSolver(createHotColdSolver(), testedPoint, RESPONSE_TEXT_VERY_HOT_WARMER, foundLocations);
testSolver(createHotColdSolver(), testedPoint, RESPONSE_TEXT_VERY_HOT_SAME_TEMP, foundLocations);
}
@Test
public void testSameTempNoChanges()
{
final HotColdSolver solver = createHotColdSolver();
final WorldPoint testedPoint = new WorldPoint(2851, 2955, 0);
final Set<HotColdLocation> foundLocations = Sets.immutableEnumSet(
HotColdLocation.KARAMJA_KHARAZI_NE,
HotColdLocation.KARAMJA_KHARAZI_SW);
testSolver(solver, testedPoint, RESPONSE_TEXT_VERY_HOT, foundLocations);
testSolver(solver, testedPoint, RESPONSE_TEXT_VERY_HOT_SAME_TEMP, foundLocations);
}
@Test
public void testNoChangesAfterSolutionFound()
{
final HotColdSolver solver = createHotColdSolver();
final Set<HotColdLocation> intermediateFoundLocations = Sets.immutableEnumSet(
HotColdLocation.KARAMJA_KHARAZI_NE,
HotColdLocation.KARAMJA_KHARAZI_SW);
final Set<HotColdLocation> finalLocation = Sets.immutableEnumSet(HotColdLocation.KARAMJA_KHARAZI_NE);
testSolver(solver, new WorldPoint(2851, 2955, 0), RESPONSE_TEXT_VERY_HOT, intermediateFoundLocations);
testSolver(solver, new WorldPoint(2852, 2955, 0), RESPONSE_TEXT_VERY_HOT_WARMER, finalLocation);
testSolver(solver, new WorldPoint(2851, 2955, 0), RESPONSE_TEXT_VERY_HOT_COLDER, finalLocation);
testSolver(solver, new WorldPoint(2465, 3495, 0), RESPONSE_TEXT_ICE_COLD_COLDER, finalLocation);
testSolver(solver, new WorldPoint(3056, 3291, 0), RESPONSE_TEXT_VERY_COLD_WARMER, finalLocation);
testSolver(solver, new WorldPoint(2571, 2956, 0), RESPONSE_TEXT_VERY_COLD_WARMER, finalLocation);
}
@Test
public void testNarrowToFindSolutions()
{
final HotColdSolver solver = createHotColdSolver();
final Set<HotColdLocation> firstLocationsSet = Sets.immutableEnumSet(
HotColdLocation.FELDIP_HILLS_GNOME_GLITER,
HotColdLocation.FELDIP_HILLS_RED_CHIN,
HotColdLocation.KARAMJA_KHARAZI_NE,
HotColdLocation.KARAMJA_CRASH_ISLAND);
final Set<HotColdLocation> secondLocationsSet = firstLocationsSet.stream()
.filter(location -> location != HotColdLocation.FELDIP_HILLS_RED_CHIN)
.collect(Collectors.toSet());
final Set<HotColdLocation> thirdLocationSet = secondLocationsSet.stream()
.filter(location -> location != HotColdLocation.FELDIP_HILLS_GNOME_GLITER)
.collect(Collectors.toSet());
final Set<HotColdLocation> finalLocation = thirdLocationSet.stream()
.filter(location -> location != HotColdLocation.KARAMJA_CRASH_ISLAND)
.collect(Collectors.toSet());
testSolver(solver, new WorldPoint(2711, 2803, 0), RESPONSE_TEXT_COLD, firstLocationsSet);
testSolver(solver, new WorldPoint(2711, 2802, 0), RESPONSE_TEXT_COLD_SAME_TEMP, firstLocationsSet);
testSolver(solver, new WorldPoint(2716, 2802, 0), RESPONSE_TEXT_COLD_WARMER, secondLocationsSet);
testSolver(solver, new WorldPoint(2739, 2808, 0), RESPONSE_TEXT_COLD_WARMER, thirdLocationSet);
testSolver(solver, new WorldPoint(2810, 2757, 0), RESPONSE_TEXT_COLD_COLDER, finalLocation);
}
@Test
public void testSomewhatDistantLocations()
{
// Activate device on Ape Atoll when solution point is HotColdLocation.KARAMJA_KHARAZI_NE
testSolver(createHotColdSolver(), new WorldPoint(2723, 2743, 0), RESPONSE_TEXT_COLD,
Sets.immutableEnumSet(
HotColdLocation.KARAMJA_KHARAZI_NE,
HotColdLocation.KARAMJA_KHARAZI_SW,
HotColdLocation.KARAMJA_CRASH_ISLAND,
HotColdLocation.FELDIP_HILLS_SW,
HotColdLocation.FELDIP_HILLS_RANTZ,
HotColdLocation.FELDIP_HILLS_RED_CHIN,
HotColdLocation.FELDIP_HILLS_SE));
// Activate device near fairy ring DKP when solution point is HotColdLocation.KARAMJA_KHARAZI_NE
testSolver(createHotColdSolver(), new WorldPoint(2900, 3111, 0), RESPONSE_TEXT_COLD,
Sets.immutableEnumSet(
HotColdLocation.KARAMJA_WEST_BRIMHAVEN,
HotColdLocation.KARAMJA_KHARAZI_NE,
HotColdLocation.ASGARNIA_COW,
HotColdLocation.ASGARNIA_CRAFT_GUILD,
HotColdLocation.KANDARIN_WITCHHAVEN,
HotColdLocation.MISTHALIN_DRAYNOR_BANK));
// Activate device on Mudskipper Point when solution point is HotColdLocation.KARAMJA_KHARAZI_NE
testSolver(createHotColdSolver(), new WorldPoint(2985, 3106, 0), RESPONSE_TEXT_COLD,
Sets.immutableEnumSet(
HotColdLocation.KARAMJA_BRIMHAVEN_FRUIT_TREE,
HotColdLocation.KARAMJA_KHARAZI_NE,
HotColdLocation.ASGARNIA_COW,
HotColdLocation.ASGARNIA_CRAFT_GUILD,
HotColdLocation.MISTHALIN_LUMBRIDGE_2,
HotColdLocation.DESERT_BEDABIN_CAMP));
}
@Test
public void testIsFirstPointCloserRect()
{
assertFalse(isFirstPointCloserRect(new WorldPoint(0, 0, 0), new WorldPoint(0, 0, 0), new Rectangle(0, 0, 1, 1)));
assertFalse(isFirstPointCloserRect(new WorldPoint(1, 0, 0), new WorldPoint(5, 0, 0), new Rectangle(2, 1, 5, 5)));
assertFalse(isFirstPointCloserRect(new WorldPoint(1, 0, 0), new WorldPoint(0, 0, 0), new Rectangle(2, 0, 1, 2)));
assertFalse(isFirstPointCloserRect(new WorldPoint(0, 0, 0), new WorldPoint(1, 1, 1), new Rectangle(2, 2, 2, 2)));
assertFalse(isFirstPointCloserRect(new WorldPoint(0, 0, 0), new WorldPoint(4, 4, 4), new Rectangle(1, 1, 2, 2)));
assertFalse(isFirstPointCloserRect(new WorldPoint(3, 2, 0), new WorldPoint(1, 5, 0), new Rectangle(0, 0, 4, 4)));
assertTrue(isFirstPointCloserRect(new WorldPoint(1, 1, 0), new WorldPoint(0, 1, 0), new Rectangle(2, 0, 3, 2)));
assertTrue(isFirstPointCloserRect(new WorldPoint(4, 4, 0), new WorldPoint(1, 1, 0), new Rectangle(3, 3, 2, 2)));
assertTrue(isFirstPointCloserRect(new WorldPoint(3, 2, 0), new WorldPoint(7, 0, 0), new Rectangle(1, 3, 4, 2)));
}
@Test
public void testIsFirstPointCloser()
{
assertFalse(isFirstPointCloser(new WorldPoint(0, 0, 0), new WorldPoint(0, 0, 0), new WorldPoint(0, 0, 0)));
assertFalse(isFirstPointCloser(new WorldPoint(0, 0, 0), new WorldPoint(0, 0, 1), new WorldPoint(0, 0, 0)));
assertFalse(isFirstPointCloser(new WorldPoint(1, 0, 0), new WorldPoint(0, 0, 0), new WorldPoint(1, 1, 0)));
assertFalse(isFirstPointCloser(new WorldPoint(2, 2, 0), new WorldPoint(0, 0, 0), new WorldPoint(1, 1, 0)));
assertTrue(isFirstPointCloser(new WorldPoint(1, 0, 0), new WorldPoint(0, 0, 0), new WorldPoint(2, 0, 0)));
assertTrue(isFirstPointCloser(new WorldPoint(1, 1, 0), new WorldPoint(1, 0, 0), new WorldPoint(2, 2, 0)));
assertTrue(isFirstPointCloser(new WorldPoint(1, 1, 1), new WorldPoint(0, 1, 0), new WorldPoint(1, 1, 0)));
}
/**
* Tests a hot-cold solver by signalling a test point, temperature, and temperature change to it and asserting the
* resulting possible location set is equal to that of a given set of expected locations.
*
* @param solver The hot-cold solver to signal to.
* <br>
* Note: This will mutate the passed solver, which is helpful for testing
* multiple sequential steps.
* @param testPoint The {@link WorldPoint} where the signal occurs.
* @param deviceResponse The string containing the temperature and temperature change which is
* given when the hot-cold checking device is activated.
* @param expectedRemainingPossibleLocations A {@link Set} of {@link HotColdLocation}s which is expected to be
* given by {@link HotColdSolver#getPossibleLocations()} after it receives
* the signal formed by the other given arguments.
*/
private static void testSolver(final HotColdSolver solver, final WorldPoint testPoint, final String deviceResponse, final Set<HotColdLocation> expectedRemainingPossibleLocations)
{
final HotColdTemperature temperature = HotColdTemperature.of(deviceResponse);
final HotColdTemperatureChange temperatureChange = HotColdTemperatureChange.of(deviceResponse);
assertNotNull(temperature);
assertEquals(expectedRemainingPossibleLocations, solver.signal(testPoint, temperature, temperatureChange));
}
/**
* @return A hot-cold solver with a starting set of master hot-cold locations nearby the KARAMJA_KHARAZI_NE
* location. {@link HotColdLocation#values()} is not used as it may change with future game updates, and
* such changes would break this test suite.
*/
private static HotColdSolver createHotColdSolver()
{
final Set<HotColdLocation> hotColdLocations = Sets.immutableEnumSet(
HotColdLocation.KARAMJA_KHARAZI_NE,
HotColdLocation.KARAMJA_KHARAZI_SW,
HotColdLocation.KARAMJA_GLIDER,
HotColdLocation.KARAMJA_MUSA_POINT,
HotColdLocation.KARAMJA_BRIMHAVEN_FRUIT_TREE,
HotColdLocation.KARAMJA_WEST_BRIMHAVEN,
HotColdLocation.KARAMJA_CRASH_ISLAND,
HotColdLocation.DESERT_BEDABIN_CAMP,
HotColdLocation.DESERT_MENAPHOS_GATE,
HotColdLocation.DESERT_POLLNIVNEACH,
HotColdLocation.DESERT_SHANTY,
HotColdLocation.MISTHALIN_LUMBRIDGE,
HotColdLocation.MISTHALIN_LUMBRIDGE_2,
HotColdLocation.MISTHALIN_DRAYNOR_BANK,
HotColdLocation.ASGARNIA_COW,
HotColdLocation.ASGARNIA_PARTY_ROOM,
HotColdLocation.ASGARNIA_CRAFT_GUILD,
HotColdLocation.ASGARNIA_RIMMINGTON,
HotColdLocation.ASGARNIA_MUDSKIPPER,
HotColdLocation.KANDARIN_WITCHHAVEN,
HotColdLocation.KANDARIN_NECRO_TOWER,
HotColdLocation.KANDARIN_FIGHT_ARENA,
HotColdLocation.KANDARIN_TREE_GNOME_VILLAGE,
HotColdLocation.FELDIP_HILLS_GNOME_GLITER,
HotColdLocation.FELDIP_HILLS_JIGGIG,
HotColdLocation.FELDIP_HILLS_RANTZ,
HotColdLocation.FELDIP_HILLS_RED_CHIN,
HotColdLocation.FELDIP_HILLS_SE,
HotColdLocation.FELDIP_HILLS_SOUTH,
HotColdLocation.FELDIP_HILLS_SW
);
return new HotColdSolver(hotColdLocations);
}
}