From f72a44dfeee8a6ed17dcb25898af0988672bbebd Mon Sep 17 00:00:00 2001 From: dtookey Date: Sat, 12 Aug 2023 07:16:24 -0400 Subject: [PATCH] cleaned up a whole bunch of stuff and started unit tests on helper functions --- src/main/kotlin/controllers/Automaton.kt | 2 +- .../kotlin/controllers/InputController.kt | 13 +++-- src/main/kotlin/controllers/Orchestrator.kt | 4 +- src/main/kotlin/controllers/RobotAutomaton.kt | 48 +++++++------------ .../kotlin/controllers/TemporalController.kt | 48 +++++++++---------- .../kotlin/game_logic/runescape/RSAgent.kt | 8 ++-- src/main/kotlin/util/HelperFunctions.kt | 25 ++++++++++ src/test/kotlin/controllers/AutomatonTest.kt | 3 +- .../controllers/DesktopControllerTest.kt | 1 - .../controllers/TemporalControllerTest.kt | 32 +++++-------- src/test/kotlin/util/HelperFunctionsTest.kt | 29 +++++++++++ 11 files changed, 121 insertions(+), 92 deletions(-) create mode 100644 src/test/kotlin/util/HelperFunctionsTest.kt diff --git a/src/main/kotlin/controllers/Automaton.kt b/src/main/kotlin/controllers/Automaton.kt index e2af7f1..06b3d91 100644 --- a/src/main/kotlin/controllers/Automaton.kt +++ b/src/main/kotlin/controllers/Automaton.kt @@ -19,7 +19,7 @@ package controllers * * 1. Obtaining an Automaton instance bound to the current OS/desktop * 2. Calling methods like [moveMouse] and [mouseClick] to perform actions - * 3. Using [sleep] and [sleepWithVariance] to add delays + * 3. Using [sleep] and [sleep] to add delays * * This interface allows the underlying OS/desktop implementation details to be abstracted and swapped as needed. */ diff --git a/src/main/kotlin/controllers/InputController.kt b/src/main/kotlin/controllers/InputController.kt index b4258c9..545a9e5 100644 --- a/src/main/kotlin/controllers/InputController.kt +++ b/src/main/kotlin/controllers/InputController.kt @@ -41,19 +41,18 @@ interface InputController { /** * Performs a mousewheel scroll in motion. * - * This will move the scroll wheel forward by the number of ticks - * over the duration. It will sleep for short intervals between - * ticks using the provided sleep duration and variance. + * This will move the scroll wheel forward by the number of ticks over the duration. It will sleep for short + * intervals between ticks using the provided sleep duration and variance. * - * @param sleepDur The base sleep duration between scroll ticks. - * @param sleepDurVariance The variance in sleep duration. + * @param sleepDurationMillis The base sleep duration between scroll ticks. + * @param sleepDurationVarianceMillis The variance in sleep duration. */ - fun scrollIn(sleepDur: Long, sleepDurVariance: Long) + fun scrollIn(sleepDurationMillis: Long, sleepDurationVarianceMillis: Long) /** * Performs a mousewheel scroll out motion. * * Same as [scrollIn] but moves the scroll wheel backward. */ - fun scrollOut(sleepDur: Long, sleepDurVariance: Long) + fun scrollOut(sleepDurationMillis: Long, sleepDurationVarianceMillis: Long) } \ No newline at end of file diff --git a/src/main/kotlin/controllers/Orchestrator.kt b/src/main/kotlin/controllers/Orchestrator.kt index e69cbe3..8253968 100644 --- a/src/main/kotlin/controllers/Orchestrator.kt +++ b/src/main/kotlin/controllers/Orchestrator.kt @@ -101,10 +101,10 @@ interface Orchestrator { */ fun moveMouseLeftClickAndSleep(p: Point, sleepDuration: Long, sleepDurationVariance: Long = 1) { automaton.moveMouse(p) - automaton.sleepWithVariance(100, 50) + automaton.sleep(100, 50) //left click automaton.mouseClick(InputEvent.BUTTON1_DOWN_MASK) - automaton.sleepWithVariance(sleepDuration, sleepDurationVariance) + automaton.sleep(sleepDuration, sleepDurationVariance) } /** diff --git a/src/main/kotlin/controllers/RobotAutomaton.kt b/src/main/kotlin/controllers/RobotAutomaton.kt index ae3a545..507fd52 100644 --- a/src/main/kotlin/controllers/RobotAutomaton.kt +++ b/src/main/kotlin/controllers/RobotAutomaton.kt @@ -2,7 +2,6 @@ package controllers import java.awt.Point import java.awt.Robot -import java.awt.event.InputEvent /** @@ -20,10 +19,9 @@ open class RobotAutomaton(internal val robot: Robot = Robot()) : Automaton { * * Uses the Robot's mouseMove() method to move to the x, y coordinates. * - * Due to a bug in OpenJDK, the mouse may not move all the way to the given - * point if using a non-default Windows display scale. So this method checks - * the current mouse position after moving and retries up to 10 times until - * the mouse reaches the exact intended point. + * Due to a bug in OpenJDK, the mouse may not move all the way to the given point if using a non-default Windows + * display scale. So this method checks the current mouse position after moving and retries up to 10 times until the + * mouse reaches the exact intended point. Multiple retries will reach the desired destination typically within 3 tries. * * @param point The Point representing the x, y coordinates to move the mouse to. */ @@ -53,15 +51,7 @@ open class RobotAutomaton(internal val robot: Robot = Robot()) : Automaton { /** * Performs a mouse click with the given button. * - * Uses the Robot to simulate pressing and releasing the mouse button. - * - * A random sleep is added after pressing the button before releasing, - * to add variance in timing and avoid robotic behavior. - * - * Valid button values are: - * - InputEvent.BUTTON1 (left click) - * - InputEvent.BUTTON2 (right click) - * - InputEvent.BUTTON3 (middle click) + * A random sleep is added after pressing the button before releasing. * * @param button the mouse button to click */ @@ -73,7 +63,7 @@ open class RobotAutomaton(internal val robot: Robot = Robot()) : Automaton { robot.mousePress(button) // Add random sleep to vary timing - sleepWithVariance(8, 8) + sleep(8, 8) robot.mouseRelease(button) } @@ -81,8 +71,6 @@ open class RobotAutomaton(internal val robot: Robot = Robot()) : Automaton { /** * Presses and releases the given key. * - * Uses the Robot's keyPress() and keyRelease() methods to simulate pressing and releasing the key. - * * A random sleep is added after pressing the key before releasing, to add variance in timing and avoid robotic * behavior. The worst-case delay is ~1 frame at 60fps. * @@ -97,7 +85,7 @@ open class RobotAutomaton(internal val robot: Robot = Robot()) : Automaton { robot.keyPress(keyCode) // Add random sleep to vary timing - sleepWithVariance(8, 8) + sleep(8, 8) robot.keyRelease(keyCode) } @@ -106,28 +94,28 @@ open class RobotAutomaton(internal val robot: Robot = Robot()) : Automaton { * Scrolls the mouse wheel down by one unit. * * Uses the [Robot.mouseWheel] method to scroll down and then sleeps for a random duration between the given - * sleepDur + rand(0, sleepDurVariance) ms. This is to add variance in scrolling speed and avoid robotic behavior. + * sleepDurationMillis + rand(0, sleepDurationVarianceMillis) ms. This adds variance in scrolling speed. * - * @param sleepDur The average duration to sleep after scrolling. - * @param sleepDurVariance The variance in sleep duration. + * @param sleepDurationMillis The minimum duration to sleep after scrolling + * @param sleepDurationVarianceMillis The maximum allowed additional sleep duration */ - override fun scrollOut(sleepDur: Long, sleepDurVariance: Long) { + override fun scrollOut(sleepDurationMillis: Long, sleepDurationVarianceMillis: Long) { robot.mouseWheel(1) - sleepWithVariance(sleepDur, sleepDurVariance) + sleep(sleepDurationMillis, sleepDurationVarianceMillis) } /** - * Scrolls the mouse wheel up by one unit. + * Scrolls the mouse wheel down by one unit. * - * Uses the [Robot.mouseWheel] method to scroll up and then sleeps for a random duration between the given - * sleepDur + rand(0, sleepDurVariance) ms. This adds variance in scrolling speed to avoid robotic behavior. + * Uses the [Robot.mouseWheel] method to scroll down and then sleeps for a random duration between the given + * sleepDurationMillis + rand(0, sleepDurationVarianceMillis) ms. This adds variance in scrolling speed. * - * @param sleepDur The average duration to sleep after scrolling - * @param sleepDurVariance The variance in sleep duration + * @param sleepDurationMillis The minimum duration to sleep after scrolling + * @param sleepDurationVarianceMillis The maximum allowed additional sleep duration */ - override fun scrollIn(sleepDur: Long, sleepDurVariance: Long) { + override fun scrollIn(sleepDurationMillis: Long, sleepDurationVarianceMillis: Long) { robot.mouseWheel(-1) - sleepWithVariance(sleepDur, sleepDurVariance) + sleep(sleepDurationMillis, sleepDurationVarianceMillis) } } \ No newline at end of file diff --git a/src/main/kotlin/controllers/TemporalController.kt b/src/main/kotlin/controllers/TemporalController.kt index 80801a5..1b43f17 100644 --- a/src/main/kotlin/controllers/TemporalController.kt +++ b/src/main/kotlin/controllers/TemporalController.kt @@ -3,38 +3,36 @@ package controllers import java.util.concurrent.TimeUnit import kotlin.random.Random +/** + * Interface for components that control time-related behavior. + * + * Implementations of this interface allow pausing execution for certain durations, + * optionally with random variance. + */ interface TemporalController { - /** - * Sleeps for the specified duration. - * - * This uses [TimeUnit.MILLISECONDS] to sleep for the given duration in milliseconds. - * - * @param dur The sleep duration in milliseconds. - */ - fun sleep(dur: Long) { - TimeUnit.MILLISECONDS.sleep(dur) - } /** - * Sleeps for the specified duration with some variance. + * Sleep for a specified base duration, with optional random variance added. * - * This will sleep for the given duration plus a random variance between 0 inclusive and [variance] exclusive. - * The variance is divided in half to generate a random positive value that is added to the duration. + * Random variance is approximated to a normal distribution and guarantees not to pause for longer than + * [maxAdditionalDuration]. * - * If the duration is negative or the variance is less than 1, this method - * will return immediately without sleeping. - * - * @param duration The base sleep duration in ms - * @param variance The amount of variance to add in ms. Gets divided in half - * and rolled as two separate random numbers to create a normal distribution + * @param baseDuration the minimum time to sleep + * @param maxAdditionalDuration maximum extra random duration to add + * @return Unit */ - fun sleepWithVariance(duration: Long, variance: Long) { - if (duration < 0 || variance <= 1) { + fun sleep(baseDuration: Long, maxAdditionalDuration: Long = 0L) { + // Check for invalid input + if (baseDuration < 0 || maxAdditionalDuration < 0) { return } - val dSize = (variance) / 2 - val r1 = Random.nextLong(dSize) - val r2 = Random.nextLong(dSize) - sleep(duration + r1 + r2) + // If no variance, sleep the base duration + val duration = if(maxAdditionalDuration == 0L){ + baseDuration + }else{ + baseDuration + util.HelperFunctions.getRandomLongFromNormalDistribution((maxAdditionalDuration) / 2) + } + + TimeUnit.MILLISECONDS.sleep(duration) } } \ No newline at end of file diff --git a/src/main/kotlin/game_logic/runescape/RSAgent.kt b/src/main/kotlin/game_logic/runescape/RSAgent.kt index 5f6953a..b2ff171 100644 --- a/src/main/kotlin/game_logic/runescape/RSAgent.kt +++ b/src/main/kotlin/game_logic/runescape/RSAgent.kt @@ -1,8 +1,8 @@ package game_logic.runescape import controllers.Automaton +import controllers.MouseWiggleParams import controllers.RobotAutomaton -import params.MouseWiggleParams import params.StandingTaskParams import params.TravelTaskParams import java.awt.Point @@ -98,7 +98,7 @@ class RSAgent(override val automaton: Automaton = RobotAutomaton()) : RSOrchestr //press the "accept" default hotkey automaton.keyPress(KeyEvent.VK_SPACE) //wait for the desired time to finish - automaton.sleepWithVariance(taskParams.craftingWaitDurationMillis, taskParams.craftingWaitDurationVarianceMillis) + automaton.sleep(taskParams.craftingWaitDurationMillis, taskParams.craftingWaitDurationVarianceMillis) } /** @@ -147,7 +147,7 @@ class RSAgent(override val automaton: Automaton = RobotAutomaton()) : RSOrchestr automaton.keyPress(KeyEvent.VK_SPACE) //wait for it to complete - automaton.sleepWithVariance(taskParams.craftingWaitDurationMillis, taskParams.craftingWaitDurationVarianceMillis) + automaton.sleep(taskParams.craftingWaitDurationMillis, taskParams.craftingWaitDurationVarianceMillis) } @@ -201,7 +201,7 @@ class RSAgent(override val automaton: Automaton = RobotAutomaton()) : RSOrchestr override fun sleepForNTicks(n: Long) { val latencyPadding = LATENCY_PADDING_MS val baseWaitTime = n * TICK_DURATION_MS - automaton.sleepWithVariance(latencyPadding + baseWaitTime, 150) + automaton.sleep(latencyPadding + baseWaitTime, 150) } } \ No newline at end of file diff --git a/src/main/kotlin/util/HelperFunctions.kt b/src/main/kotlin/util/HelperFunctions.kt index 74b9b04..235f06f 100644 --- a/src/main/kotlin/util/HelperFunctions.kt +++ b/src/main/kotlin/util/HelperFunctions.kt @@ -1,6 +1,7 @@ package util import controllers.Orchestrator +import kotlin.random.Random /** * A collection of helper functions for common utility tasks. @@ -32,6 +33,30 @@ object HelperFunctions { } + /** + * Generates a random long value approximating a normal distribution. + * + * Takes two random samples from 0 to upperBound/2 and adds them together. This approximates a normal distribution + * better than a single random sample. + * + * @param upperBound The upper bound to sample from + * @return A random long value following an approximate normal distribution + */ + fun getRandomLongFromNormalDistribution(upperBound: Long): Long{ + //anything lower to 2 will round down to zero, so return zero + if(upperBound <= 2L){ + return 0L + } + + // To create more natural distribution Take two random samples from 0 to upperBound/2 and add them. This + // approximates a normal distribution better than single random sample. + val subBound = upperBound/2L + val result1 = Random.nextLong(subBound) + val result2 = Random.nextLong(subBound) + return result1 + result2 + } + + /** * Prints a progress report to console showing current step, total steps, elapsed time, and estimated remaining time. * diff --git a/src/test/kotlin/controllers/AutomatonTest.kt b/src/test/kotlin/controllers/AutomatonTest.kt index 83eaea1..eb8b20c 100644 --- a/src/test/kotlin/controllers/AutomatonTest.kt +++ b/src/test/kotlin/controllers/AutomatonTest.kt @@ -2,7 +2,6 @@ package controllers import org.mockito.Mockito.mock import org.mockito.Mockito.`when` -import params.MouseWiggleParams import java.awt.Point import java.awt.event.InputEvent import kotlin.test.Test @@ -75,7 +74,7 @@ class AutomatonTest { val automaton = mock(Automaton::class.java) automaton.sleep(1000) - automaton.sleepWithVariance(1000, 200) + automaton.sleep(1000, 200) // Asserts Automaton extends TemporalController } } diff --git a/src/test/kotlin/controllers/DesktopControllerTest.kt b/src/test/kotlin/controllers/DesktopControllerTest.kt index dc50387..ba7f5dc 100644 --- a/src/test/kotlin/controllers/DesktopControllerTest.kt +++ b/src/test/kotlin/controllers/DesktopControllerTest.kt @@ -1,6 +1,5 @@ package controllers -import params.MouseWiggleParams import kotlin.test.* import org.mockito.Mockito.* import java.awt.Point diff --git a/src/test/kotlin/controllers/TemporalControllerTest.kt b/src/test/kotlin/controllers/TemporalControllerTest.kt index 0ece881..ef1e016 100644 --- a/src/test/kotlin/controllers/TemporalControllerTest.kt +++ b/src/test/kotlin/controllers/TemporalControllerTest.kt @@ -12,30 +12,22 @@ internal class TemporalControllerTest { * Creates an instance of TemporalController for testing. */ private val controller = object : TemporalController { - - /** - * Sleeps for the given duration in milliseconds. - */ - override fun sleep(dur: Long) { - TimeUnit.MILLISECONDS.sleep(dur) - } - /** * Sleeps for around the given duration, with variance. * - * @param duration the desired duration to sleep - * @param variance the amount of variance in the actual duration + * @param baseDuration the desired duration to sleep + * @param maxAdditionalDuration the amount of variance in the actual duration */ - override fun sleepWithVariance(duration: Long, variance: Long) { - if (duration < 0 || variance <= 1) { + override fun sleep(baseDuration: Long, maxAdditionalDuration: Long) { + if (baseDuration < 0 || maxAdditionalDuration <= 1) { return } - val dSize = (variance) / 2 + val dSize = (maxAdditionalDuration) / 2 val r1 = Random.nextLong(dSize) val r2 = Random.nextLong(dSize) - sleep(duration + r1 + r2) + sleep(baseDuration + r1 + r2) } } @@ -53,7 +45,7 @@ internal class TemporalControllerTest { } /** - * Tests that [TemporalController.sleepWithVariance] sleeps for approximately + * Tests that [TemporalController.sleep] sleeps for approximately * the given duration, within the specified variance. */ @Test @@ -62,7 +54,7 @@ internal class TemporalControllerTest { val variance = 500L val start = System.currentTimeMillis() - controller.sleepWithVariance(duration, variance) + controller.sleep(duration, variance) val end = System.currentTimeMillis() val elapsed = end - start @@ -74,13 +66,13 @@ internal class TemporalControllerTest { } /** - * Tests that [TemporalController.sleepWithVariance] returns immediately + * Tests that [TemporalController.sleep] returns immediately * if passed a negative duration. */ @Test fun `sleepWithVariance returns immediately if duration is negative`() { val start = System.currentTimeMillis() - controller.sleepWithVariance(-100, 500) + controller.sleep(-100, 500) val end = System.currentTimeMillis() val elapsed = end - start @@ -88,13 +80,13 @@ internal class TemporalControllerTest { } /** - * Tests that [TemporalController.sleepWithVariance] returns immediately + * Tests that [TemporalController.sleep] returns immediately * if the variance parameter is 0. */ @Test fun `sleepWithVariance returns immediately if variance is 0`() { val start = System.currentTimeMillis() - controller.sleepWithVariance(100, 0) + controller.sleep(100, 0) val end = System.currentTimeMillis() val elapsed = end - start diff --git a/src/test/kotlin/util/HelperFunctionsTest.kt b/src/test/kotlin/util/HelperFunctionsTest.kt new file mode 100644 index 0000000..3b9a7aa --- /dev/null +++ b/src/test/kotlin/util/HelperFunctionsTest.kt @@ -0,0 +1,29 @@ +package util + +import kotlin.math.pow +import kotlin.test.* + + +class HelperFunctionsTest { + + + @Test + fun `test getRandomLongFromNormalDistribution generates normal distribution`() { + + // Generate a large number of samples + val numSamples = 10000 + val upperBound = 1000L + val samples = (1..numSamples).map { + HelperFunctions.getRandomLongFromNormalDistribution(upperBound) + } + + // Calculate mean and standard deviation + val mean = samples.average() + val stdDev = samples.map { (it - mean).pow(2) }.average().pow(0.5) + + // Assert mean and std dev are within expected range + assert(mean > upperBound * 0.4 && mean < upperBound * 0.6) + assert(stdDev > upperBound * 0.2 && stdDev < upperBound * 0.3) + + } +} \ No newline at end of file