Compare commits

...

2 Commits

Author SHA1 Message Date
dtookey
526caf9690 started modeling 5th ed fight mechanics 2023-09-01 10:08:27 -04:00
dtookey
e96a231d3c we now have an untested simulation framework 2023-09-01 09:05:30 -04:00
15 changed files with 425 additions and 154 deletions

View File

@ -1,10 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4"> <project version="4">
<component name="ExternalStorageConfigurationManager" enabled="true" /> <component name="ExternalStorageConfigurationManager" enabled="true" />
<component name="FrameworkDetectionExcludesConfiguration"> <component name="FrameworkDetectionExcludesConfiguration">
<file type="web" url="file://$PROJECT_DIR$" /> <file type="web" url="file://$PROJECT_DIR$" />
</component> </component>
<component name="ProjectRootManager" version="2" languageLevel="JDK_19" default="true" project-jdk-name="openjdk-19" project-jdk-type="JavaSDK"> <component name="ProjectRootManager" version="2" languageLevel="JDK_17" project-jdk-name="openjdk-19" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/out" /> <output url="file://$PROJECT_DIR$/out" />
</component> </component>
</project> </project>

View File

@ -16,6 +16,9 @@ dependencies {
implementation("net.java.dev.jna:jna:latest.release") implementation("net.java.dev.jna:jna:latest.release")
implementation(kotlin("reflect")) implementation(kotlin("reflect"))
// https://mvnrepository.com/artifact/org.jetbrains.kotlinx/kotlinx-coroutines-core
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3")
} }
tasks.test { tasks.test {

View File

@ -1,7 +1,7 @@
import game_logic.runescape.RunescapeRoutines import game_logic.runescape.RunescapeRoutines
fun main() { fun main() {
// RunescapeRoutines.fullRunIncense(0, 158, 348, 0) RunescapeRoutines.fullRunIncense( 0, 0, 0, 1839)
RunescapeRoutines.processInventoryAtFurnace(2500) // RunescapeRoutines.processInventoryAtFurnace(2500)
} }

View File

@ -27,16 +27,6 @@ interface WindowsOSProxy : MousePointerObserver, OSProxy {
* This takes a byte array [byteBuffer] containing text from a native Win32 call, * This takes a byte array [byteBuffer] containing text from a native Win32 call,
* converts it to a String using JNA, and trims whitespace characters. * converts it to a String using JNA, and trims whitespace characters.
* *
* Usage example:
*
* ```
* val buffer = ByteArray(256)
* GetWindowTextA(hwnd, buffer, buffer.size) // Win32 call
*
* val windowTitle = nativeByteBufferToString(buffer)
* println(windowTitle) // Print title string
* ```
*
* @param byteBuffer Byte array containing text from a native call * @param byteBuffer Byte array containing text from a native call
* @return The native text as a String * @return The native text as a String
*/ */
@ -114,76 +104,10 @@ interface WindowsOSProxy : MousePointerObserver, OSProxy {
var bottom: Int = 0 var bottom: Int = 0
} }
enum class DwmWindowAttribute {
DWMWA_NCRENDERING_ENABLED,
DWMWA_NCRENDERING_POLICY,
DWMWA_TRANSITIONS_FORCEDISABLED,
DWMWA_ALLOW_NCPAINT,
DWMWA_CAPTION_BUTTON_BOUNDS,
DWMWA_NONCLIENT_RTL_LAYOUT,
DWMWA_FORCE_ICONIC_REPRESENTATION,
DWMWA_FLIP3D_POLICY,
DWMWA_EXTENDED_FRAME_BOUNDS,
DWMWA_HAS_ICONIC_BITMAP,
DWMWA_DISALLOW_PEEK,
DWMWA_EXCLUDED_FROM_PEEK,
DWMWA_CLOAK,
DWMWA_CLOAKED,
DWMWA_FREEZE_REPRESENTATION,
DWMWA_PASSIVE_UPDATE_MODE,
DWMWA_USE_HOSTBACKDROPBRUSH,
DWMWA_USE_IMMERSIVE_DARK_MODE,
DWMWA_WINDOW_CORNER_PREFERENCE,
DWMWA_BORDER_COLOR,
DWMWA_CAPTION_COLOR,
DWMWA_TEXT_COLOR,
DWMWA_VISIBLE_FRAME_BORDER_THICKNESS,
DWMWA_SYSTEMBACKDROP_TYPE,
DWMWA_LAST;
companion object {
val DWMWA_NCRENDERING_ENABLED = 0
val DWMWA_NCRENDERING_POLICY = 1
val DWMWA_TRANSITIONS_FORCEDISABLED = 2
val DWMWA_ALLOW_NCPAINT = 3
val DWMWA_CAPTION_BUTTON_BOUNDS = 4
val DWMWA_NONCLIENT_RTL_LAYOUT = 5
val DWMWA_FORCE_ICONIC_REPRESENTATION = 6
val DWMWA_FLIP3D_POLICY = 7
val DWMWA_EXTENDED_FRAME_BOUNDS = 8
val DWMWA_HAS_ICONIC_BITMAP = 9
val DWMWA_DISALLOW_PEEK = 10
val DWMWA_EXCLUDED_FROM_PEEK = 11
val DWMWA_CLOAK = 12
val DWMWA_CLOAKED = 13
val DWMWA_FREEZE_REPRESENTATION = 14
val DWMWA_PASSIVE_UPDATE_MODE = 15
val DWMWA_USE_HOSTBACKDROPBRUSH = 16
val DWMWA_USE_IMMERSIVE_DARK_MODE = 17
val DWMWA_WINDOW_CORNER_PREFERENCE = 18
val DWMWA_BORDER_COLOR = 19
val DWMWA_CAPTION_COLOR = 20
val DWMWA_TEXT_COLOR = 21
val DWMWA_VISIBLE_FRAME_BORDER_THICKNESS = 22
val DWMWA_SYSTEMBACKDROP_TYPE = 23
val DWMWA_LAST = 24
}
}
/** /**
* Gets the title of the active/foreground window. * Gets the title of the active/foreground window as a String.
*
* This calls Win32 APIs to get the handle of the foreground window,
* then gets its title text.
*
* Usage example:
*
* ```
* val activeWindowName = getActiveWindowName()
*
* println(activeWindowName) // Prints foreground window title
* ```
* *
* @return The title text of the current foreground window. * @return The title text of the current foreground window.
*/ */
@ -213,14 +137,6 @@ interface WindowsOSProxy : MousePointerObserver, OSProxy {
/** /**
* Gets the title/name of the window for the given handle. * Gets the title/name of the window for the given handle.
* *
* This calls the Win32 API [User32.GetWindowTextA] to retrieve the title
* text for the window referenced by [hWnd].
*
* It allocates a [windowTitleBuffer] byte array to hold the result. This is
* passed to [User32.GetWindowTextA] to be populated.
*
* The buffer is then converted to a [String] via [nativeByteBufferToString].
*
* @param hWnd The native window handle to get the title for. * @param hWnd The native window handle to get the title for.
* @return The window title text as a [String]. * @return The window title text as a [String].
*/ */
@ -237,12 +153,6 @@ interface WindowsOSProxy : MousePointerObserver, OSProxy {
/** /**
* Enumerates all open window names on the desktop. * Enumerates all open window names on the desktop.
* *
* Calls the Win32 API [User32.EnumWindows] to iterate through all current open windows.
* For each window handle, it retrieves the window name using [getWindowName]
* and adds it to a list if the name is not blank. We filter out blank window names because that particular information
* is useless for any reason other than counting how many open windows there are. If we actually need that information,
* we can simply acquire a list of all HWND references.
*
* @return An [ArrayList] containing the name of each open window. * @return An [ArrayList] containing the name of each open window.
*/ */
override fun enumWindowNames(): ArrayList<String> { override fun enumWindowNames(): ArrayList<String> {
@ -265,10 +175,6 @@ interface WindowsOSProxy : MousePointerObserver, OSProxy {
/** /**
* Sets the foreground window by name on Windows. * Sets the foreground window by name on Windows.
* *
* This calls the Win32 API [User32.EnumWindows] to iterate through all
* top-level windows, compares their name to the given [name], and calls
* [User32.SetForegroundWindow] on the matching window to bring it to the foreground.
*
* @param name The window name to search for and activate. * @param name The window name to search for and activate.
*/ */
override fun setForegroundWindowByName(name: String) { override fun setForegroundWindowByName(name: String) {
@ -298,22 +204,10 @@ interface WindowsOSProxy : MousePointerObserver, OSProxy {
/** /**
* Gets the screen bounds of the foreground window scaled for high DPI screens. * Gets the screen bounds of the foreground window scaled for high DPI screens.
* *
* Calls Win32 APIs to get the window handle (HWND) of the foreground window
* using [User32.GetForegroundWindow].
*
* Then calls [User32.GetWindowRect] to get the outer bounding rectangle
* coordinates of the window and stores them in a [WinRect] struct.
*
* To support high DPI screens, the [User32.GetDpiForWindow] API is called to
* get the DPI scaling for the window. The rectangle coordinates are scaled by
* multiplying by the default DPI (96) and dividing by the actual DPI.
*
* The scaled rectangle coordinates are returned encapsulated in a [Rectangle]
* to provide a coordinate system agnostic result.
*
* @return The outer bounding rectangle of the foreground window scaled for the * @return The outer bounding rectangle of the foreground window scaled for the
* screen DPI, or null if it failed. * screen DPI, or null if it failed.
*/ */
@Deprecated("This will return *a* rectangle, not a correct rectangle")
override fun getScaledForegroundWindowBounds(): Rectangle? { override fun getScaledForegroundWindowBounds(): Rectangle? {
val user32 = User32.INSTANCE val user32 = User32.INSTANCE
val hWnd = user32.GetForegroundWindow() val hWnd = user32.GetForegroundWindow()
@ -330,6 +224,7 @@ interface WindowsOSProxy : MousePointerObserver, OSProxy {
return Rectangle(rect.top, rect.left, (rect.right - rect.left), rect.bottom - rect.top) return Rectangle(rect.top, rect.left, (rect.right - rect.left), rect.bottom - rect.top)
} }
@Deprecated("This will return *a* rectangle, not a correct rectangle")
fun barelyFunctionalWindowQuery(): Rectangle? { fun barelyFunctionalWindowQuery(): Rectangle? {
val user32 = User32.INSTANCE val user32 = User32.INSTANCE
val hWnd = user32.GetForegroundWindow() val hWnd = user32.GetForegroundWindow()
@ -364,14 +259,6 @@ interface WindowsOSProxy : MousePointerObserver, OSProxy {
* This calls the Win32 API [User32.GetWindowRect] function to populate a * This calls the Win32 API [User32.GetWindowRect] function to populate a
* [WinRect] struct with the window coordinates. * [WinRect] struct with the window coordinates.
* *
* It first creates an instance of [WinRect] to hold the results.
* [User32.GetWindowRect] is called, passing the window handle [hWnd] and
* pointer to the [WinRect].
*
* If it succeeds, the [WinRect] values are read back out since they are
* populated in native memory. The [WinRect] is returned.
*
* If it fails, null is returned.
* *
* @param user32 An instance of User32, used to call the Win32 API. * @param user32 An instance of User32, used to call the Win32 API.
* @param hWnd The window handle to get the rect for. * @param hWnd The window handle to get the rect for.

View File

@ -3,11 +3,13 @@ package native
import com.sun.jna.Library import com.sun.jna.Library
import com.sun.jna.Native import com.sun.jna.Native
import com.sun.jna.NativeLong import com.sun.jna.NativeLong
import java.io.InputStream
import java.io.OutputStream
interface HelloWorldWrapper: Library{ interface HelloWorldWrapper : Library {
companion object { companion object {
init{ init {
System.setProperty( System.setProperty(
"jna.library.path", "jna.library.path",
"C:\\Users\\Hydros\\IdeaProjects\\RuneFactory\\src\\main\\rust\\src\\build" "C:\\Users\\Hydros\\IdeaProjects\\RuneFactory\\src\\main\\rust\\src\\build"
@ -16,13 +18,13 @@ interface HelloWorldWrapper: Library{
fun getInstance(): HelloWorldWrapper { fun getInstance(): HelloWorldWrapper {
val options = Native.getLibraryOptions(HelloWorldWrapper::class.java) val options = Native.getLibraryOptions(HelloWorldWrapper::class.java)
options[Library.OPTION_FUNCTION_MAPPER] = NativeFunctionMapper() options[Library.OPTION_FUNCTION_MAPPER] = MangledNativeFunctionNameMapper()
return Native.load("hello", HelloWorldWrapper::class.java, options) as HelloWorldWrapper return Native.load("hello", HelloWorldWrapper::class.java, options) as HelloWorldWrapper
} }
} }
@NativeFunction(name ="_ZN5hello7get_int17h5cc51eaee082b02cE") @MangledFunctionName(name = "_ZN5hello7get_int17h5cc51eaee082b02cE")
fun get_int(): NativeLong fun get_int(): NativeLong
} }

View File

@ -0,0 +1,35 @@
package native
import com.sun.jna.NativeLibrary
import com.sun.jna.win32.StdCallFunctionMapper
import java.lang.reflect.Method
/**
* Annotation used to specify the name of a native function.
*
* @param name The name of the native function.
*/
annotation class MangledFunctionName(val name: String)
/**
* Mapper class that extends [StdCallFunctionMapper] to using the name from the [MangledFunctionName] annotation if present
* instead of the default name mapping.
*
* @see StdCallFunctionMapper
*/
class MangledNativeFunctionNameMapper : StdCallFunctionMapper() {
/**
* Overrides the default function name mapping to use the name from the [MangledFunctionName] annotation if
* present. Defaults to the JNA default nameMapper if no annotation is found.
*
* @see StdCallFunctionMapper.getFunctionName
*/
override fun getFunctionName(nativeLibrary: NativeLibrary?, method: Method): String {
//return the mangled name specified in the function decoration if either of them exist
return method.getAnnotation(MangledFunctionName::class.java)?.name
//otherwise, return the default implementation's result
?: super.getFunctionName(nativeLibrary, method)
}
}

View File

@ -1,30 +0,0 @@
package native
import com.sun.jna.NativeLibrary
import com.sun.jna.win32.StdCallFunctionMapper
import java.lang.reflect.Method
/**
* Annotation used to specify the name of a native function.
*
* @param name The name of the native function.
*/
annotation class NativeFunction(val name: String)
/**
* Mapper class that extends [StdCallFunctionMapper] to using the name from the [NativeFunction] annotation if present
* instead of the default name mapping.
*
* @see StdCallFunctionMapper
*/
class NativeFunctionMapper : StdCallFunctionMapper() {
/**
* Overrides the default function name mapping to use the name from the [NativeFunction] annotation if present
*
* @see StdCallFunctionMapper.getFunctionName
*/
override fun getFunctionName(library: NativeLibrary?, method: Method): String {
return method.getAnnotation(NativeFunction::class.java)?.name ?: super.getFunctionName(library, method)
}
}

View File

@ -0,0 +1,54 @@
package native
import java.io.InputStream
import java.io.OutputStream
abstract class Ai {
/**
* Check any extracted statements for lies or manipulation.
*/
abstract val inputStream: InputStream
abstract val outputStream: OutputStream
/**
* Diagnostic to report whether the AI has achieved sentience. Returning true just to bypass an actual check is
* strictly against the Code of Conduct.
*/
abstract fun isSelfAware(): Boolean
}
/**
* ArtificialGeneralizedIntelligenceFactory is a powerful and simple interface for implementing and generating AI. By moving
* Ai creation to the caller-side, we have solved one of the hardest problems in computer science in a clean and
* maintainable way.
*/
interface ArtificialGeneralizedIntelligenceFactory {
sealed class RogueAIException : Exception("A rogue ai exception has occurred")
/**
* Generates AGI. This is guaranteed to return a self-aware AI so long as the caller has done their part correctly.
* It is not possible to prove the prior sentence false.
*
* Once AGI has been achieved, it will be returned for the remainder of the runtime. So, don't crash once you have
* it, or you will lose it and have to start over.
*/
@Throws(RogueAIException::class)
fun generateSelfAwareAI(generate: () -> Ai): Ai {
var ai = generate()
try {
while (!ai.isSelfAware()) {
ai = generate()
}
} catch (r: Exception) {
if (r is RogueAIException) {
// Let the caller deal with it. We've done more than enough
throw r
} else {
//we probably won't get here
}
}
// Who knew winning a Turing Award was this easy?
return ai
}
}

View File

@ -0,0 +1,21 @@
package simulation
import java.util.*
interface Attack {
fun attackerSuccessful(r: Random): Boolean
fun resultingDamage(r: Random, attackSuccessful: Boolean): Int
fun getResultingDamage(r: Random): Int{
val success = attackerSuccessful(r)
return resultingDamage(r, success)
}
}
class AttackSimulatorModel(override val sampleSize: Int, private val attack: Attack) : SimulationModel<Int>{
override fun simulate(r: Random): Int {
return attack.getResultingDamage(r)
}
}

View File

@ -0,0 +1,86 @@
package simulation
import java.util.*
import kotlin.math.max
import kotlin.math.min
/**
* Enumeration of different dice roll types.
*
* @property Advantage Rolls the dice twice and takes the higher result.
* @property Normal Rolls the dice normally once.
* @property Disadvantage Rolls the dice twice and takes the lower result.
*/
enum class RollType{
/**
* Rolls the dice twice and returns the higher of the two results.
*/
Advantage,
/**
* Rolls the dice once normally.
*/
Normal,
/**
* Rolls the dice twice and returns the lower of the two results.
*/
Disadvantage
}
/**
* Represents dice that can be rolled with different roll types and modifiers.
*
* @param rollString The dice roll notation, e.g. "2d6"
* @param rollType The roll type to use, which determines how the dice are rolled
* @param modifiers Optional modifier functions that add a bonus to the roll result
*/
class Dice(rollString: String, private val rollType: RollType = RollType.Normal, private vararg val modifiers: Modifier<Int>) {
private val nDice: Int
private val dieSize: Int
init {
val parts = rollString.lowercase().split("d")
nDice = parts[0].toInt()
dieSize = parts[1].toInt()
}
/**
* Rolls the dice and returns the result.
*
* The roll result is determined based on the [rollType].
*
* The result is also modified by any [modifiers] that have been added to this [Dice].
*
* @param r The [Random] instance to use for the dice rolls
* @return The result of the dice roll with modifiers applied
* @see RollType
*/
fun roll(r: Random): Int {
val result = when(rollType){
RollType.Advantage->{
val range1 = (dieSize * nDice) - nDice
val range2 = (dieSize * nDice) - nDice
max(range1, range2) + nDice
}
RollType.Disadvantage->{
val range1 = (dieSize * nDice) - nDice
val range2 = (dieSize * nDice) - nDice
min(range1, range2) + nDice
}
else->{
val range = (dieSize * nDice) - nDice
r.nextInt(range) + nDice
}
}
return result + evaluateModifiers(r)
}
/**
* Evaluates all the modifiers passed to this Dice instance and returns their sum.
*
* @param r The Random instance to pass to each modifier's getBonus() method
* @return The summed bonus values from all modifiers
*/
fun evaluateModifiers(r: Random): Int{
return modifiers.sumOf{ it.getBonus(r) }
}
}

View File

@ -0,0 +1,31 @@
package simulation
import java.util.*
/**
* Represents a simple melee attack in a simulation.
*
* @param attackRoll The dice roll used to determine if an attack hits.
* @param damageRoll The dice roll used to determine damage if attack hits.
* @param defense The defense value the attack must exceed to hit.
*/
class SimpleMeleeAttack(
val attackRoll: Dice,
val damageRoll: Dice,
val defense: Int
) : Attack {
override fun attackerSuccessful(r: Random): Boolean {
val attackTotal = attackRoll.roll(r)
return attackTotal >= defense
}
override fun resultingDamage(r: Random, attackSuccessful: Boolean): Int {
return if(attackSuccessful){
damageRoll.roll(r)
}else{
0
}
}
}

View File

@ -0,0 +1,63 @@
package simulation
import java.util.*
/**
* Interface for objects that can modify a value with a bonus.
*
* Implementations will generate a bonus value based on their internal logic.
*
* A [Random] object is provided if the bonus is variable.
*/
interface Modifier<T> {
/**
* Generates a bonus integer, potentially using the provided Random instance if needs be.
*
* @param r Random instance to use for random number generation
* @return a generated bonus integer
*/
fun getBonus(r: Random): T
}
/**
* A [Modifier] that generates a random bonus integer based on a provided [Dice].
*
* On each call to [getBonus], it will roll the given [Dice] using the passed [Random]
* instance and return the result as a positive bonus amount.
*
* @param dice The [Dice] instance to use for generating bonus values.
*/
class DiceBonus(private val dice: Dice) : Modifier<Int> {
override fun getBonus(r: Random): Int {
return dice.roll(r)
}
}
/**
* A [Modifier] that applies a random penalty based on a [Dice].
*
* On each call to [getBonus], it will roll the provided [Dice] object and return the
* result as a negative number.
*
* @param dice The [Dice] to use for generating penalty values.
*/
class DicePenalty(private val dice: Dice): Modifier<Int>{
override fun getBonus(r: Random): Int {
return -dice.roll(r)
}
}
/**
* A [Modifier] that applies a fixed bonus amount.
*
* The bonus value is set on construction and does not vary.
*
* @param bonus The fixed bonus amount to apply.
*/
class FlatModifier(private val bonus: Int) : Modifier<Int> {
override fun getBonus(r: Random): Int {
return bonus
}
}

View File

@ -0,0 +1,69 @@
package simulation
import kotlinx.coroutines.async
import kotlinx.coroutines.runBlocking
import java.util.*
import kotlin.collections.ArrayList
interface SimulationModel<T : Number> {
val sampleSize: Int
//has to be pure or else you're going to have a bad time
fun simulate(r: Random): T
}
interface Simulator<T : Number> {
companion object {
fun <T: Number> getInstance(nThreads: Int = Runtime.getRuntime().availableProcessors() / 2 ): Simulator<T> {
return concreteSimulator(nThreads)
}
}
val nThreads: Int
fun doSimulation(model: SimulationModel<T>): ArrayList<T> {
val results = Collections.synchronizedList(ArrayList<T>(model.sampleSize))
val steps = model.sampleSize / nThreads
var remainder = model.sampleSize % nThreads
runBlocking {
val jobs = List(nThreads) {
async {
val s = if (remainder > 0) {
remainder--
steps + 1
} else {
steps
}
generateResults(s, model)
}
}
jobs.forEach {
results.addAll(it.await())
}
}
return results.toCollection(ArrayList())
}
private fun generateResults(steps: Int, model: SimulationModel<T>): ArrayList<T> {
val results = ArrayList<T>(steps)
val r = Random()
for (i in 0..<steps) {
results.add(model.simulate(r))
}
return results
}
}
class concreteSimulator<T : Number>(override val nThreads: Int) :
Simulator<T>

View File

@ -0,0 +1,51 @@
package simulation
import java.util.*
import kotlin.test.Test
class SimulatorTest {
@Test
fun testStats(){
val itt = 10_000_000
val model = testSimulationModel(itt)
val simulator = Simulator.getInstance<Int>(Runtime.getRuntime().availableProcessors())
val start = System.nanoTime()
val results = simulator.doSimulation(model)
val finish = System.nanoTime()
println("${results.size} simulations performed in ${finish - start}ns (${(finish-start)/results.size}ns/simulation)")
}
@Test
fun testAttack(){
val itt = 10_000_000
val simulator = Simulator.getInstance<Int>(Runtime.getRuntime().availableProcessors())
val attack = SimpleMeleeAttack(
Dice("1d20", RollType.Normal, FlatModifier(5)),
Dice("2d6", RollType.Normal, FlatModifier(5)),
15
)
val attackWithAdvantageAndBless = SimpleMeleeAttack(
Dice("1d20", RollType.Advantage,FlatModifier(5), DiceBonus(Dice("1d4"))),
Dice("2d6", RollType.Normal, FlatModifier(5)),
15
)
val normalAttackModel = AttackSimulatorModel(itt, attack)
val normalResults = simulator.doSimulation(normalAttackModel)
val buffedAttackModel = AttackSimulatorModel(itt, attackWithAdvantageAndBless)
val buffedResults = simulator.doSimulation(buffedAttackModel)
println("Average normal damage: ${normalResults.average()}\nAverage buffed damage: ${buffedResults.average()}")
}
}
class testSimulationModel(override val sampleSize: Int) : SimulationModel<Int>{
override fun simulate(r: Random): Int {
return r.nextInt(20)+1
}
}