Compare commits

...
Sign in to create a new pull request.

86 commits

Author SHA1 Message Date
65bdce0140 Add zzlib 2021-05-13 20:24:57 -04:00
707bfb0059 Fix potential problem with input and output pipes 2021-05-13 20:10:56 -04:00
ed5e64c6b7 Handle threading in the wrapper 2021-05-13 05:55:09 -04:00
097f1cbfb4 Add instruction about ROM path 2021-05-13 04:26:01 -04:00
aae1d30eb9 Remove instructions to load the ROM. It should load automatically 2021-05-13 04:23:52 -04:00
ce0639fd40 Use builtin ZIP writing functionality for performance reasons. 2021-05-13 04:21:47 -04:00
3756979b6a Increase performance by disabling some layers 2021-05-13 03:02:49 -04:00
f4229bfc48 Add option to auto load and save the pool file 2021-05-12 22:24:59 -04:00
0646e57826 Update link 2021-05-12 20:03:23 -04:00
7ef36d98f2 random logic remove 2021-05-09 00:29:01 -04:00
8233847f46 Pass sprites around 2021-05-08 23:44:18 -04:00
22b41082a7 Properly bubble up the max fitness because some calculations use it. 2021-05-08 09:31:11 -04:00
6e718eb3b0 Don't use separate timeout for vertical levels since more movements are
counted towards resetting the timer.
2021-05-08 07:01:47 -04:00
30ada92db7 Add config option to turn off UI to try to speed things up. 2021-05-08 06:41:51 -04:00
d224ba7805 Extra argument 2021-05-08 00:42:50 -04:00
0d9afa4ced Paranoid distrust of variable scoping 2021-05-08 00:07:52 -04:00
bd5b9cc4cb Log model writes. 2021-05-07 23:43:15 -04:00
2e53f92179 Fix possible issue with iteration variable changing 2021-05-07 23:35:10 -04:00
32778cf9fd Added a bunch of code to add waypoints to the goal. Not sure at what
point this becomes cheating.
2021-05-07 21:54:25 -04:00
eb53739618 Fix bitness of velocity. Added target scan function. Currently broken. 2021-05-07 03:35:13 -04:00
7fcb93c83c Automatically start the ROM in single thread mode. 2021-05-06 23:58:21 -04:00
55d0bff81c Don't reset timeout if already dropped 2021-05-06 05:05:25 -04:00
cf4842e124 Detect falling before bonus screen reset 2021-05-06 05:00:41 -04:00
d6cf4c1505 Consolidate UI text 2021-05-06 04:29:13 -04:00
603dcf502c Goal offset indicator 2021-05-06 02:13:35 -04:00
3f872c169c Severe punishment for falling 2021-05-06 01:40:17 -04:00
75c3ef6f4b Update layout 2021-05-06 00:48:57 -04:00
ca2496f7d5 Allow repeat species again since there's no chance of accidentally
rereading the file.
2021-05-05 20:01:16 -04:00
5a12540259 Added i3 layout for whoever may want it 2021-05-05 18:46:34 -04:00
5ed93c28da Added save state 2021-05-05 18:25:42 -04:00
0c42d8452c Don't change the timeout if we're already timed out 2021-05-05 18:22:16 -04:00
1c1307a0e2 Don't kill the run on the bonus intro screen 2021-05-05 18:18:59 -04:00
0a59ab8b21 Fix reset functionality 2021-05-05 08:38:43 -04:00
fda82e0cd7 More advanced logic to calculate good tiles which allows some ropes. 2021-05-05 05:33:35 -04:00
1370bfd9a1 Environment variable to load pool file 2021-05-05 03:34:04 -04:00
80ab0405ec Check text on error 2021-05-05 00:41:09 -04:00
1cd3618b6f Add climbing. Fix sprite list regression 2021-05-04 23:29:31 -04:00
123d7508d1 Fix pipes in Linux with socat 2021-05-04 20:30:52 -04:00
a5b089520b Clean up some old garbage 2021-05-04 08:36:50 -04:00
6c1c4fa0eb Named pipes in Windows. It is glorious. 2021-05-04 08:20:35 -04:00
542682e126 Added Mainbrace Mayhem state 2021-05-04 01:46:41 -04:00
2a96f6d9eb Handle banana wrapping 2021-05-03 05:20:52 -04:00
317578611d Ignore when the party x or y values are zero. This is likely wrong. 2021-05-03 04:39:15 -04:00
52bdebfc81 Find the exits by moving the characters and camera in a grid pattern 2021-05-03 03:23:04 -04:00
529f5dc67a Remove loady 2021-05-02 02:29:26 -04:00
aa6f8babc8 Reduce number of reads for sprite region 2021-05-02 01:08:32 -04:00
4d078da6f8 Hide Play Top temporarily 2021-05-01 23:28:28 -04:00
d90c03c3af Fix loading pools. Move input modification outside main loop so we can
use the timer for Promise synchronization.
2021-05-01 23:17:59 -04:00
91cf9ec489 Include terminal window in CSS selector 2021-05-01 22:23:16 -04:00
b5f3084ddc Run the check for Xpra in the background 2021-05-01 22:16:45 -04:00
478fa59509 Fix Xpra script 2021-05-01 22:04:23 -04:00
42a220d39d Remove total genomes status message 2021-05-01 22:03:09 -04:00
2fb5c2494c Fix idiotic color code 2021-05-01 21:47:20 -04:00
aa8d27959c Change how the timer is dispatched based on the threadedness of the
application.
2021-05-01 21:25:38 -04:00
836fc09ce8 Fix status line 2021-05-01 20:50:13 -04:00
6149f72d98 Make sure the promises aren't dropping frames 2021-05-01 19:55:31 -04:00
63f7147458 Remove debug junk 2021-05-01 19:40:33 -04:00
c7819c98f2 Promisify everything? 2021-05-01 19:39:35 -04:00
088c92113b Add sprite index 2021-04-30 19:12:13 -04:00
3e767915dd Added command example for bsnes-launcher script 2021-04-30 19:07:54 -04:00
6a777089ff Fix problems with displaying help 2021-04-30 18:45:40 -04:00
60511c1cc4 Attempt to fix genome graph so it doesn't destroy the whole run 2021-04-30 04:42:55 -04:00
d2f7a1a5d8 Reduce how often the UI renders for the overlay tool 2021-04-30 03:20:53 -04:00
8fbc113a3e Ignore bst 2021-04-30 02:22:57 -04:00
c7bd873452 More tweaks to bsnes script 2021-04-30 02:21:08 -04:00
7fed47983d Add instructions about when to preload the ROM vs not. 2021-04-30 01:07:47 -04:00
fa94206df2 Fix more paths 2021-04-30 00:58:54 -04:00
88718d391d Don't download watchexec on Linux 2021-04-30 00:54:35 -04:00
c70fc5a97b Fix paths 2021-04-30 00:51:51 -04:00
7fd2a75581 Make tools directory. Reorganize README. 2021-04-30 00:43:46 -04:00
8b3d5fd9b1 Added tool to make debugging sprites in bsnes-plus easier 2021-04-30 00:32:39 -04:00
a9090d18b9 Unused variable 2021-04-29 20:31:58 -04:00
2d0426b323 Typo in donkutil 2021-04-29 20:31:08 -04:00
4158876854 Move a ton of constants into a separate file. 2021-04-29 20:19:56 -04:00
5c03d02c0e Add option to disable sound 2021-04-29 09:32:55 -04:00
e1b6f2937b More Windows performance tweaks 2021-04-29 08:51:03 -04:00
391ec2ae9e Added watchexec to remove Powershell dependency 2021-04-29 07:22:08 -04:00
ea6b102678 More useless tweaks to fix Windows 2021-04-29 05:33:47 -04:00
5857d14824 README Update 2021-04-29 05:02:03 -04:00
ae28a6110b Attempt to fix some Windows issues 2021-04-29 04:56:55 -04:00
75ae2d8ae7 Reduce thread count 2021-04-28 23:32:06 -04:00
d88af17c09 Truncation 2021-04-28 23:29:43 -04:00
36e7e75213 Proper event handling for Linux. Xpra CSS 2021-04-28 23:18:26 -04:00
5ea3c30457 Reuse processes for more efficiency 2021-04-28 18:54:39 -04:00
689c680be8 Fix more Windows issues 2021-04-28 04:33:52 -04:00
c591641233 Added promise library, serpent.load, temp logic 2021-04-28 04:05:16 -04:00
29 changed files with 3526 additions and 4498 deletions

View file

@ -2,4 +2,4 @@
indent_size = 4
indent_style = space
charset = utf-8
end_of_line = lf
end_of_line = lf

7
.gitignore vendored
View file

@ -1,3 +1,5 @@
*.lsmv
!pool/*.lsmv
*.log
catchem/
state/
@ -6,4 +8,9 @@ crashsave*
*.pool*
*.lsvs
config.lua
*.srm
*.sfc
*.bst
namedpipe*
build/
.VSCodeCounter/

7
LICENSE.namedpipe.md Normal file
View file

@ -0,0 +1,7 @@
Copyright © 2014 Peter S. May
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

File diff suppressed because it is too large Load diff

View file

@ -2,25 +2,45 @@
An AI based on SethBling's MarI/O to play Donkey Kong Country 2 with lsnes.
See [YouTube](https://www.youtube.com/watch?v=Q69_wmEkp-k) for an example run.
See [YouTube](https://www.youtube.com/watch?v=-\_UyUbObLeE) for an example run.
## Requirements
* lsnes with **Lua 5.2** (do not try to build with 5.3, it does not work!)
* socat for Linux, or a fairly recent version of Windows that has PowerShell
* A Donkey Kong Country 2 1.1 US ROM (matching hash b79c2bb86f6fc76e1fc61c62fc16d51c664c381e58bc2933be643bbc4d8b610c)
### Windows
You will want to install the rrtest-1613424691 version of lsnes. Older versions were crashing for me. The easiest way to do this is to use Chocolatey:
```powershell
# Make sure you use an Administrator shell!
# Skip this command if you have Chocolatey already.
Set-ExecutionPolicy Bypass -Scope Process -Force; [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor 3072; iex ((New-Object System.Net.WebClient).DownloadString('https://chocolatey.org/install.ps1'))
# Install lsnes
choco install --version 2.0.24-rrtest-1613424691 lsnes
```
## Instructions
1. Start lsnes
2. Go to `Configure -> Settings -> Advanced` and change `LUA -> Maximum memory use` to `1024MB`
3. Load the DKC2 ROM: `File -> Load -> ROM...`
4. Load the `neat-donk.lua` script: `Tools -> Run Lua script...`
5. You may also want to turn off sound since it may get annoying. `Configure -> Sounds enabled`
6. Look at config.lua for some settings you can change. Not all have been tested, but you should be able to change the number on the `_M.Filename =` line to get a different state file from the `_M.State` list. Also note the `Threads =` line. Change this to 1 to prevent multiple instances of lsnes from getting launched at once. If you use more than 1 thread, you may also want to launch `lsnes` using xpra to manage the windows, with the `xpra-run.sh` script.
3. Load the `neat-donk.lua` script: `Tools -> Run Lua script...`
If you want a better idea of what's going on with the tile and sprite calculations you may want to load `donkutil.lua`. It will mark the tiles with their offsets on the screen, give a crosshair with tile measurements (every 32 pixels), and list information about the sprites (you can use the 1 and 2 keys above the letter keys to page through them). Sprites labeled in green are considered "good", red is "bad", normal color is neutral. Solid red means that it's the active sprite in the info viewer.
## Config
<img src="https://github.com/empathicqubit/neat-donk/blob/master/doc/donkutil.png?raw=true" />
Look at config.lua for some settings you can change. Not all have been tested.
* `_M.ROM`: The ROM path, `rom.sfc` by default.
* `_M.Filename`: Change the number to a different one from the `_M.State` list
to load a different file.
* `_M.NeatConfig.Threads`: Change this to 1 to prevent multiple instances of
lsnes from getting launched at once, or increase it to run more instances.
If you use more than 1 thread, you may also want to launch `lsnes` using xpra
to manage the windows, with the [xpra-run.sh](xpra-run.sh) script.
## Keys
1: Stop/start
@ -33,9 +53,29 @@ If you want a better idea of what's going on with the tile and sprite calculatio
9: Restart
## Other Tools
### Status Overlay
The status overlay is located at [tools/status-overlay.lua](tools/status-overlay.lua).
It will help you see the tile and sprite calculations by marking the tiles with
their offsets on the screen, giving a crosshair with tile measurements every
32 pixels, and listing information about the sprites. You can use the 1 and 2
keys above the letter keys to page through them. Sprites labeled in green are
considered "good", red is "bad", normal color is neutral. Solid red means that
it's the active sprite in the info viewer.
<img src="https://github.com/empathicqubit/neat-donk/blob/master/doc/donkutil.png?raw=true" />
### BSNES Launcher
Located at [tools/bsnes-launcher.lua](tools/bsnes-launcher.lua), this script
gives you an easy way to launch bsnes-plus with breakpoints preset. Run it in
lsnes and it will display a message to the Lua console and stderr on how to use it.
## Notes
* Only tested on Pirate Panic
* The pool files are gzipped Serpent data
* The pool files are PKZIP files with one file, data.serpent, containing Serpent-formatted data
## Credits
@ -44,9 +84,12 @@ If you want a better idea of what's going on with the tile and sprite calculatio
* [Basic tilemap info from p4plus2/DKC2-disassembly](https://github.com/p4plus2/DKC2-disassembly)
* [Serpent](https://github.com/pkulchenko/serpent)
* [LibDeflate](https://github.com/SafeteeWoW/LibDeflate)
* [Billiam's Promise library](https://github.com/Billiam/promise.lua)
* [https://github.com/psmay/windows-named-pipe-utils](https://github.com/psmay/windows-named-pipe-utils)
## TODO
- [x] Incur penalty for non-hazardous enemy collisions to encourage neutralizing Klobber
- [ ] Award for picking up items
- [ ] Make enemies neutral when held? (Klobber, Click-Clack, etc.)
- [ ] Multiple nets to handle different contexts s/a clicking map items

View file

@ -17,12 +17,18 @@ _M.ROM = _M.ScriptDir .. "/rom.sfc"
Rearrange for other savestates. (will be redone soon)
--]]
_M.State = {
"PiratePanic.lsmv",
"PiratePanicDitch.lsmv",
"PiratePanicKremcoin.lsmv",
-- W1.1 Pirate Panic
"PiratePanic.lsmv", -- [1]
"PiratePanicDitch.lsmv", -- [2]
"PiratePanicKremcoin.lsmv", --[3]
-- W1.2 Mainbrace Mayhem
"MainbraceMayhem.lsmv", -- [4]
"MainbraceMayhemBonus.lsmv", -- [5]
"MainbraceMayhemTopOfRope.lsmv", -- [6]
}
_M.Filename = _M.PoolDir .. _M.State[1]
_M.Filename = _M.PoolDir .. _M.State[4]
--[[
Start game with specific powerup.
@ -35,9 +41,13 @@ _M.Filename = _M.PoolDir .. _M.State[1]
_M.StartPowerup = 0
_M.NeatConfig = {
Threads = 8,
DisableSound = true,
Threads = 7,
ShowInterface = false,
AutoSave = true,
--Filename = "DP1.state",
SaveFile = _M.Filename .. ".pool",
SaveFile = _M.PoolDir .. "bigbrain.pool",
Filename = _M.Filename,
Population = 300,
DeltaDisjoint = 2.0,
@ -53,7 +63,7 @@ BiasMutationChance = 0.40,
StepSize = 0.1,
DisableMutationChance = 0.4,
EnableMutationChance = 0.2,
TimeoutConstant = 20,
TimeoutConstant = 30,
MaxNodes = 1000000,
}

534
game.lua
View file

@ -1,10 +1,15 @@
--Notes here
local memory, bit, memory2, input, callback, movie, utime = memory, bit, memory2, input, callback, movie, utime
local base = string.gsub(@@LUA_SCRIPT_FILENAME@@, "(.*[/\\])(.*)", "%1")
local Promise = nil
local util = nil
local mathFunctions = dofile(base.."/mathFunctions.lua")
local config = dofile(base.."/config.lua")
local spritelist = dofile(base.."/spritelist.lua")
local util = dofile(base.."/util.lua")
local mem = dofile(base.."/mem.lua")
local _M = {
leader = 0,
tilePtr = 0,
@ -22,42 +27,41 @@ local _M = {
spritelist.InitSpriteList()
spritelist.InitExtSpriteList()
local KREMCOINS = 0x7e08cc
local TILE_SIZE = 32
local ENEMY_SIZE = 64
local TILE_COLLISION_MATH_POINTER = 0x7e17b2
local SPRITE_BASE = 0x7e0de2
local SPRITE_SIZE = 94
local SPRITE_DYING = 0x1000
local VERTICAL_POINTER = 0xc414
local TILEDATA_POINTER = 0x7e0098
local HAVE_BOTH = 0x7e08c2
local CAMERA_X = 0x7e17ba
local CAMERA_Y = 0x7e17c0
local LEAD_CHAR = 0x7e08a4
local PARTY_X = 0x7e0a2a
local PARTY_Y = 0x7e0a2c
local SOLID_LESS_THAN = 0x7e00a0
local KONG_LETTERS = 0x7e0902
local MATH_LIVES = 0x7e08be
local DISPLAY_LIVES = 0x7e0c0
local MAIN_AREA_NUMBER = 0x7e08a8
local CURRENT_AREA_NUMBER = 0x7e08c8
function _M.getPositions()
_M.leader = memory.readword(LEAD_CHAR)
_M.tilePtr = memory.readhword(TILEDATA_POINTER)
_M.vertical = memory.readword(TILE_COLLISION_MATH_POINTER) == VERTICAL_POINTER
_M.partyX = memory.readword(PARTY_X)
_M.partyY = memory.readword(PARTY_Y)
_M.leader = memory.readword(mem.addr.leadChar)
_M.tilePtr = memory.readhword(mem.addr.tiledataPointer)
_M.vertical = memory.readword(mem.addr.tileCollisionMathPointer) == mem.addr.verticalPointer
_M.partyX = memory.readword(mem.addr.partyX)
_M.partyY = memory.readword(mem.addr.partyY)
_M.cameraX = memory.readword(CAMERA_X)
_M.cameraY = memory.readword(CAMERA_Y)
_M.cameraX = memory.readword(mem.addr.cameraX)
_M.cameraY = memory.readword(mem.addr.cameraY)
_M.screenX = (_M.partyX-256-_M.cameraX)*2
_M.screenY = (_M.partyY-256-_M.cameraY)*2
end
function _M.setPartyPosition(x, y)
memory.writeword(mem.addr.partyX, x)
memory.writeword(mem.addr.partyY, y)
_M.setSpritePosition(0, x, y)
_M.setSpritePosition(1, x, y)
end
function _M.setCameraPosition(x, y)
memory.writeword(mem.addr.cameraX, x)
memory.writeword(mem.addr.cameraY, y)
memory.writeword(mem.addr.cameraX2, x)
memory.writeword(mem.addr.cameraY2, y)
end
function _M.setSpritePosition(index, x, y)
local offsets = mem.offset.sprite
local spriteBase = mem.addr.spriteBase + index * mem.size.sprite
memory.writeword(spriteBase + offsets.x, x)
memory.writeword(spriteBase + offsets.y, y)
end
function _M.getBananas()
local bananas = memory.readword(0x7e08bc)
return bananas
@ -69,15 +73,202 @@ function _M.getCoins()
end
function _M.getKremCoins()
local krem = memory.readword(KREMCOINS)
local krem = memory.readword(mem.addr.kremcoins)
return krem
end
function _M.getGoalHit()
local sprites = _M.getSprites()
function _M.getAreaWidth()
return memory.readword(mem.addr.areaWidth) + 256
end
function _M.getAreaHeight()
return memory.readword(mem.addr.areaHeight)
end
function _M.getAreaLength()
return memory.readword(mem.addr.areaLength)
end
local onFrameAdvancedQueue = {}
function _M.advanceFrame()
local promise = Promise.new()
table.insert(onFrameAdvancedQueue, promise)
return promise
end
local function processFrameAdvanced()
for i=#onFrameAdvancedQueue,1,-1 do
table.remove(onFrameAdvancedQueue, i):resolve()
end
end
local onSetRewindQueue = {}
function _M.setRewindPoint()
local promise = Promise.new()
table.insert(onSetRewindQueue, promise)
movie.unsafe_rewind()
return promise
end
local function processSetRewind(state)
for i=#onSetRewindQueue,1,-1 do
table.remove(onSetRewindQueue, i):resolve(state)
end
end
local onRewindQueue = {}
function _M.rewind(rew)
local promise = Promise.new()
movie.unsafe_rewind(rew)
table.insert(onRewindQueue, promise)
return promise
end
local function processRewind()
for i=#onRewindQueue,1,-1 do
table.remove(onRewindQueue, i):resolve()
end
end
local function findPreferredExitLoop(frame, searchX, searchY, found, uniqueExits)
return _M.advanceFrame():next(function()
frame = frame + 1
if frame % 2 ~=0 then
return
end
local areaWidth = _M.getAreaWidth()
memory.writebyte(0x7e19ce, 0x16)
memory.writebyte(0x7e0e12, 0x99)
memory.writebyte(0x7e0e70, 0x99)
local sprites = _M.getSprites()
for i=1,#sprites,1 do
local sprite = sprites[i]
local name = spritelist.SpriteNames[sprite.control]
if sprite.control == spritelist.GoodSprites.goalTarget or
sprite.control == spritelist.GoodSprites.areaExit then
found = true
uniqueExits[sprite.y * areaWidth + sprite.x] = sprite
end
end
_M.setPartyPosition(searchX, searchY)
_M.setCameraPosition(searchX, searchY)
searchX = searchX + 0x100
if searchX > areaWidth then
searchX = 0
searchY = searchY + 0xe0
if searchY > _M.getAreaHeight() then
table.sort(uniqueExits, function(a, b)
return a.control < b.control
end)
-- Return upper right corner if we can't find anything
if found then
for id,sprite in pairs(uniqueExits) do
return { x = sprite.x, y = sprite.y }
end
else
return { x = areaWidth, y = 0}
end
end
end
end):next(function(ret)
if ret == nil then
return findPreferredExitLoop(frame, searchX, searchY, found, uniqueExits)
else
return ret
end
end)
end
function _M.findPreferredExit()
local point = nil
local result = nil
return _M.setRewindPoint():next(function(p)
point = p
return findPreferredExitLoop(0, 0, 0, false, {})
end):next(function(r)
result = r
return _M.rewind(point)
end):next(function()
return result
end)
end
--- Starting from a specified point in the area, weave back and forth until we reach
--- fall to the bottom. Return all points where we hit the floor on the way down.
---@param startX integer x coordinate of the starting point
---@param startY integer y coordinate of the starting point
---@return table table A list of all the points that we collided with a platform
function _M.getWaypoints(startX, startY)
local areaWidth = _M.getAreaWidth()
local areaHeight = _M.getAreaHeight()
local increment = mem.size.tile
local direction = increment
local terminus = areaWidth
-- If we're on the right half, move left
-- If we're on the left half, move right
if startX > areaWidth / 2 then
direction = -direction
terminus = 0x100
end
local collisions = {}
local currentY = startY
local currentX = startX
local switches = 0
while currentY < areaHeight - increment do
switches = 0
-- Drop down until we collide with the floor
while currentY < areaHeight - increment and _M.getAbsoluteTile(currentX, currentY) ~= 1 do
currentY = currentY + increment
end
-- Track the collision
table.insert(collisions, {
x = currentX,
y = currentY,
})
-- Break if we've hit the bottom
if currentY > areaHeight - increment then
break
end
-- Move in the direction until we reach a gap or the edge of the area
while currentY < areaHeight - increment do
currentX = currentX + direction
-- Switch directions if we're out of bounds
if direction < 0 and currentX < terminus + increment or direction > 0 and currentX > terminus - increment then
switches = switches + 1
if switches > 2 then
currentY = currentY + increment
break
end
if terminus == 0x100 then
terminus = areaWidth
direction = increment
else
terminus = 0x100
direction = -increment
end
elseif _M.getAbsoluteTile(currentX, currentY) ~= 1 then
-- Check to make sure there isn't a floor immediately underneath.
-- If there is we're probably on an incline.
if _M.getAbsoluteTile(currentX, currentY + increment) == 1 then
currentY = currentY + increment
else
break
end
end
end
end
return collisions
end
function _M.getGoalHit(sprites)
for i=1,#sprites,1 do
local sprite = sprites[i]
if sprite.control ~= 0x0164 then
if sprite.control ~= spritelist.GoodSprites.goalBarrel then
goto continue
end
-- Check if the goal barrel is moving up
@ -91,7 +282,7 @@ function _M.getGoalHit()
end
function _M.getKong()
local kong = memory.readword(KONG_LETTERS)
local kong = memory.readword(mem.addr.kongLetters)
return bit.popcount(kong)
end
@ -107,7 +298,7 @@ end
function _M.getBoth()
-- FIXME consider invincibility barrels
local both = memory.readword(HAVE_BOTH)
local both = memory.readword(mem.addr.haveBoth)
return bit.band(both, 0x4000)
end
@ -132,13 +323,12 @@ function _M.writePowerup(powerup)
-- memory.writebyte(0x0019, powerup)
end
function _M.getHit(alreadyHit)
return not alreadyHit and memory.readword(MATH_LIVES) < memory.readword(DISPLAY_LIVES)
return not alreadyHit and memory.readword(mem.addr.mathLives) < memory.readword(mem.addr.displayLives)
end
function _M.getHitTimer(lastBoth)
return (memory.readsbyte(DISPLAY_LIVES) - memory.readsbyte(MATH_LIVES))
return (memory.readsbyte(mem.addr.displayLives) - memory.readsbyte(mem.addr.mathLives))
+ lastBoth - _M.getBoth()
end
@ -180,10 +370,6 @@ function _M.tileIsSolid(x, y, tileVal, tileOffset)
return false
end
if questionable_tiles then
return true
end
local a2 = bit.band(x, 0x1f)
if bit.band(tileVal, 0x4000) ~= 0 then
@ -192,7 +378,7 @@ function _M.tileIsSolid(x, y, tileVal, tileOffset)
tileVal = bit.band(tileVal, 0x3fff)
local solidLessThan = memory.readword(SOLID_LESS_THAN)
local solidLessThan = memory.readword(mem.addr.solidLessThan)
if tileVal >= solidLessThan then
return false
@ -227,9 +413,9 @@ function _M.tileIsSolid(x, y, tileVal, tileOffset)
return true
end
function _M.getTile(dx, dy)
local tileX = math.floor((_M.partyX + dx * TILE_SIZE) / TILE_SIZE) * TILE_SIZE
local tileY = math.floor((_M.partyY + dy * TILE_SIZE) / TILE_SIZE) * TILE_SIZE
function _M.getAbsoluteTile(x, y)
local tileX = math.floor(x / mem.size.tile) * mem.size.tile
local tileY = math.floor(y / mem.size.tile) * mem.size.tile
local offset = _M.tileOffsetCalculation(tileX, tileY, _M.vertical)
@ -242,8 +428,12 @@ function _M.getTile(dx, dy)
return 1
end
function _M.getTile(dx, dy)
return _M.getAbsoluteTile(_M.partyX + dx * mem.size.tile, _M.partyY + dy * mem.size.tile)
end
function _M.getCurrentArea()
return memory.readword(CURRENT_AREA_NUMBER)
return memory.readword(mem.addr.currentAreaNumber)
end
function _M.getJumpHeight()
@ -254,28 +444,49 @@ function _M.getJumpHeight()
return sprite.jumpHeight
end
function _M.getSprite(idx)
local base_addr = idx * SPRITE_SIZE + SPRITE_BASE
function _M.diedFromHit()
local sprite = _M.getSprite(_M.leader)
if sprite == nil then
return 0
end
local control = memory.readword(base_addr)
return sprite.motion == 0x05
end
function _M.fell()
local sprite = _M.getSprite(_M.leader)
if sprite == nil then
return 0
end
return sprite.motion == 0x3b
end
function _M.getSprite(idx)
local baseAddr = idx * mem.size.sprite + mem.addr.spriteBase
local spriteData = memory.readregion(baseAddr, mem.size.sprite)
local offsets = mem.offset.sprite
local control = util.regionToWord(spriteData, offsets.control)
if control == 0 then
return nil
end
local x = memory.readword(base_addr + 0x06)
local y = memory.readword(base_addr + 0x0a)
local x = util.regionToWord(spriteData, offsets.x)
local y = util.regionToWord(spriteData, offsets.y)
local sprite = {
control = control,
screenX = x - 256 - _M.cameraX - 256,
screenY = y - 256 - _M.cameraY - 256,
jumpHeight = memory.readword(base_addr + 0x0e),
jumpHeight = util.regionToWord(spriteData, offsets.jumpHeight),
-- style bits
-- 0x4000 0: Right facing 1: Flipped
-- 0x1000 0: Alive 1: Dying
style = memory.readword(base_addr + 0x12),
velocityX = memory.readsword(base_addr + 0x20),
velocityY = memory.readsword(base_addr + 0x24),
style = util.regionToWord(spriteData, offsets.style),
velocityX = util.regionToSWord(spriteData, offsets.velocityX),
velocityY = util.regionToSWord(spriteData, offsets.velocityY),
motion = util.regionToWord(spriteData, offsets.motion),
x = x,
y = y,
good = spritelist.Sprites[control]
@ -305,9 +516,8 @@ end
-- Currently only for single bananas since they don't
-- count as regular computed sprites
function _M.getExtendedSprites()
function _M.getExtendedSprites(sprites)
local oam = memory2.OAM:readregion(0x00, 0x220)
local sprites = _M.getSprites()
local extended = {}
for idx=0,0x200/4-1,1 do
@ -328,15 +538,15 @@ function _M.getExtendedSprites()
end
-- Hide the interface icons
if screenSprite.x < 0 or screenSprite.y < TILE_SIZE then
if screenSprite.x < 0 or screenSprite.y < mem.size.tile then
goto continue
end
-- Hide sprites near computed sprites
for s=1,#sprites,1 do
local sprite = sprites[s]
if screenSprite.x > sprite.screenX - ENEMY_SIZE and screenSprite.x < sprite.screenX + ENEMY_SIZE / 2 and
screenSprite.y > sprite.screenY - ENEMY_SIZE and screenSprite.y < sprite.screenY then
if screenSprite.x > sprite.screenX - mem.size.enemy and screenSprite.x < sprite.screenX + mem.size.enemy / 2 and
screenSprite.y > sprite.screenY - mem.size.enemy and screenSprite.y < sprite.screenY then
goto continue
end
::nextsprite::
@ -349,63 +559,91 @@ function _M.getExtendedSprites()
return extended
end
callcount = 0
function _M.getInputs()
_M.getPositions()
local sprites = _M.getSprites()
local extended = _M.getExtendedSprites()
local inputs = {}
local inputDeltaDistance = {}
_M.getPositions()
local sprites = _M.getSprites()
local extended = _M.getExtendedSprites(sprites)
local inputs = {}
local inputDeltaDistance = {}
for dy = -config.BoxRadius, config.BoxRadius, 1 do
for dx = -config.BoxRadius, config.BoxRadius, 1 do
inputs[#inputs+1] = 0
inputDeltaDistance[#inputDeltaDistance+1] = 1
tile = _M.getTile(dx, dy)
if tile == 1 then
if _M.getTile(dx, dy-1) == 1 then
inputs[#inputs+1] = 0
inputDeltaDistance[#inputDeltaDistance+1] = 1
local tile = _M.getTile(dx, dy)
if tile == 1 then
if inputs[#inputs-config.BoxRadius*2-1] == -1 then
inputs[#inputs] = -1
else
inputs[#inputs] = 1
end
elseif tile == 0 and _M.getTile(dx + 1, dy) == 1 and _M.getTile(dx + 1, dy - 1) == 1 then
inputs[#inputs] = -1
end
for i = 1,#sprites do
local sprite = sprites[i]
local distx = math.abs(sprite.x - (_M.partyX+dx*TILE_SIZE))
local disty = math.abs(sprite.y - (_M.partyY+dy*TILE_SIZE))
local dist = math.sqrt((distx * distx) + (disty * disty))
if dist <= TILE_SIZE * 1.25 then
inputs[#inputs] = sprite.good
if dist > TILE_SIZE then
inputDeltaDistance[#inputDeltaDistance] = mathFunctions.squashDistance(dist)
end
end
::continue::
end
local neighbors = 0
for ddy=-1,1,1 do
for ddx=-1,1,1 do
if (ddy == 0 and ddx == 0) or (ddx == 0 and ddy == 1) then
goto continue
end
for i = 1,#extended do
local distx = math.abs(extended[i]["x"]+_M.cameraX - (_M.partyX+dx*TILE_SIZE))
local disty = math.abs(extended[i]["y"]+_M.cameraY - (_M.partyY+dy*TILE_SIZE))
if distx < TILE_SIZE / 2 and disty < TILE_SIZE / 2 then
inputs[#inputs] = extended[i]["good"]
local dist = math.sqrt((distx * distx) + (disty * disty))
if dist > TILE_SIZE / 2 then
inputDeltaDistance[#inputDeltaDistance] = mathFunctions.squashDistance(dist)
end
end
end
end
end
return inputs, inputDeltaDistance
if _M.getTile(dx+ddx, dy+ddy) == 0 then
neighbors = neighbors + 1
end
::continue::
end
end
if neighbors >= 3 then
inputs[#inputs] = 1
else
inputs[#inputs] = -1
end
end
end
for i = 1,#sprites do
local sprite = sprites[i]
if sprite.good == 0 then
goto continue
end
local distx = math.abs(sprite.x - (_M.partyX+dx*mem.size.tile))
local disty = math.abs(sprite.y - (_M.partyY+dy*mem.size.tile))
local dist = math.sqrt((distx * distx) + (disty * disty))
if dist <= mem.size.tile * 1.25 then
inputs[#inputs] = sprite.good
if dist > mem.size.tile then
inputDeltaDistance[#inputDeltaDistance] = mathFunctions.squashDistance(dist)
end
end
::continue::
end
for i = 1,#extended do
local distx = math.abs(extended[i]["x"]+_M.cameraX - (_M.partyX+dx*mem.size.tile))
local disty = math.abs(extended[i]["y"]+_M.cameraY - (_M.partyY+dy*mem.size.tile))
if distx < mem.size.tile / 2 and disty < mem.size.tile / 2 then
inputs[#inputs] = extended[i]["good"]
local dist = math.sqrt((distx * distx) + (disty * disty))
if dist > mem.size.tile / 2 then
inputDeltaDistance[#inputDeltaDistance] = mathFunctions.squashDistance(dist)
end
end
end
end
end
return inputs, inputDeltaDistance
end
function _M.getClimbing()
local sprite = _M.getSprite(_M.leader)
if sprite == nil then
return false
end
return sprite.motion >= 0x35 and sprite.motion <= 0x39
end
function _M.clearJoypad()
@ -432,8 +670,8 @@ function _M.onEmptyHit(handler)
table.insert(emptyHitQueue, handler)
end
function processEmptyHit(addr, val)
local idx = math.floor((bit.band(addr, 0xffff) - bit.band(SPRITE_BASE, 0xffff)) / SPRITE_SIZE)
local function processEmptyHit(addr, val)
local idx = math.floor((bit.band(addr, 0xffff) - bit.band(mem.addr.spriteBase, 0xffff)) / mem.size.sprite)
local pow = _M.getSprite(idx)
if pow == nil or
pow.control ~= 0x0238 then
@ -443,7 +681,7 @@ function processEmptyHit(addr, val)
local sprites = _M.getSprites()
for i=1,#sprites,1 do
local sprite = sprites[i]
if bit.band(sprite.style, SPRITE_DYING) ~= 0 and
if bit.band(sprite.style, mem.flag.sprite.dying) ~= 0 and
sprite.good == -1 then
return
end
@ -454,24 +692,76 @@ function processEmptyHit(addr, val)
end
end
function processAreaLoad()
local function processAreaLoad()
for i=#areaLoadedQueue,1,-1 do
table.remove(areaLoadedQueue, i)()
end
end
function processMapLoad()
local function processMapLoad()
for i=#mapLoadedQueue,1,-1 do
table.remove(mapLoadedQueue, i)()
end
areaLoadedQueue = {} -- We clear this because it doesn't make any sense after the map screen loads
end
function _M.bonusScreenDisplayed(inputs)
local count = 0
for i=1,#inputs,1 do
if inputs[i] ~= 0 then
count = count + 1
end
end
return count < 10
end
local handlers = {}
local function registerHandler(space, regname, addr, callback)
table.insert(handlers, {
fn = space[regname](space, addr, callback),
unregisterFn = space['un'..regname],
space = space,
addr = addr,
})
end
local inputHandler = nil
local setRewindHandler = nil
local rewindHandler = nil
function _M.unregisterHandlers()
callback.unregister('input', inputHandler)
callback.unregister('set_rewind', setRewindHandler)
callback.unregister('post_rewind', rewindHandler)
inputHandler = nil
setRewindHandler = nil
rewindHandler = nil
for i=#handlers,1,-1 do
local handler = table.remove(handlers, i)
handler.unregisterFn(handler.space, handler.addr, handler.fn)
end
end
function _M.registerHandlers()
memory2.BUS:registerwrite(0xb517b2, processAreaLoad)
memory2.WRAM:registerread(0x06b1, processMapLoad)
if inputHandler ~= nil then
error("Only call register handlers once")
end
inputHandler = callback.register('input', processFrameAdvanced)
setRewindHandler = callback.register('set_rewind', processSetRewind)
rewindHandler = callback.register('post_rewind', processRewind)
registerHandler(memory2.BUS, 'registerwrite', 0xb517b2, processAreaLoad)
registerHandler(memory2.WRAM, 'registerwrite', 0x069b, processMapLoad)
for i=2,22,1 do
memory2.WRAM:registerwrite(bit.band(SPRITE_BASE + SPRITE_SIZE * i, 0xffff), processEmptyHit)
registerHandler(memory2.WRAM, 'registerwrite', bit.band(mem.addr.spriteBase + mem.size.sprite * i, 0xffff), processEmptyHit)
end
end
return _M
return function(promise)
Promise = promise
if util == nil then
util = dofile(base.."/util.lua")(Promise)
end
return _M
end

458
i3.json Normal file
View file

@ -0,0 +1,458 @@
[
{
"border": "normal",
"floating": "auto_off",
"layout": "splith",
"percent": 1,
"type": "con",
"nodes": [
{
"border": "normal",
"floating": "auto_off",
"layout": "splitv",
"percent": 0.25,
"type": "con",
"nodes": [
{
"border": "normal",
"floating": "auto_off",
"layout": "tabbed",
"percent": 0.5,
"type": "con",
"nodes": [
{
"border": "normal",
"current_border_width": 3,
"floating": "auto_off",
"geometry": {
"height": 1030,
"width": 106,
"x": 0,
"y": 0
},
"name": " - Terminal",
"percent": 0.333333333333333,
"swallows": [
{
"title": "[Tt]erminal"
}
],
"type": "con"
},
{
"border": "normal",
"current_border_width": 3,
"floating": "auto_off",
"geometry": {
"height": 1030,
"width": 114,
"x": 0,
"y": 0
},
"name": "lsnes: Messages",
"percent": 0.333333333333333,
"swallows": [
{
"class": "^Lsnes$",
"title": "Messages"
}
],
"type": "con"
},
{
"border": "normal",
"current_border_width": 3,
"floating": "auto_off",
"geometry": {
"height": 1030,
"width": 122,
"x": 0,
"y": 0
},
"name": "lsnes rr2-β24 [null core]",
"percent": 0.333333333333333,
"swallows": [
{
"class": "^Lsnes$",
"title": "null "
}
],
"type": "con"
}
]
},
{
"border": "normal",
"floating": "auto_off",
"layout": "tabbed",
"percent": 0.5,
"type": "con",
"nodes": [
{
"border": "normal",
"current_border_width": 3,
"floating": "auto_off",
"geometry": {
"height": 1030,
"width": 1914,
"x": 0,
"y": 0
},
"name": "lsnes rr2-β24 [bsnes v085 (Compatibility core)]",
"percent": 0.5,
"swallows": [
{
"class": "^Lsnes$",
"title": "Compatibility"
}
],
"type": "con"
},
{
"border": "normal",
"current_border_width": 3,
"floating": "auto_off",
"geometry": {
"height": 1030,
"width": 634,
"x": 0,
"y": 0
},
"name": "lsnes: Messages",
"percent": 0.5,
"swallows": [
{
"class": "^Lsnes$",
"title": "Messages"
}
],
"type": "con"
}
]
}
]
},
{
"border": "normal",
"floating": "auto_off",
"layout": "splitv",
"percent": 0.25,
"type": "con",
"nodes": [
{
"border": "normal",
"floating": "auto_off",
"layout": "tabbed",
"percent": 0.5,
"type": "con",
"nodes": [
{
"border": "normal",
"current_border_width": 3,
"floating": "auto_off",
"geometry": {
"height": 1030,
"width": 954,
"x": 0,
"y": 0
},
"name": "lsnes rr2-β24 [bsnes v085 (Compatibility core)]",
"percent": 0.5,
"swallows": [
{
"class": "^Lsnes$",
"title": "Compatibility"
}
],
"type": "con"
},
{
"border": "normal",
"current_border_width": 3,
"floating": "auto_off",
"geometry": {
"height": 1030,
"width": 474,
"x": 0,
"y": 0
},
"name": "lsnes: Messages",
"percent": 0.5,
"swallows": [
{
"class": "^Lsnes$",
"title": "Messages"
}
],
"type": "con"
}
]
},
{
"border": "normal",
"floating": "auto_off",
"layout": "tabbed",
"percent": 0.5,
"type": "con",
"nodes": [
{
"border": "normal",
"current_border_width": 3,
"floating": "auto_off",
"geometry": {
"height": 1030,
"width": 378,
"x": 0,
"y": 0
},
"name": "lsnes rr2-β24 [bsnes v085 (Compatibility core)]",
"percent": 0.5,
"swallows": [
{
"class": "^Lsnes$",
"title": "Compatibility"
}
],
"type": "con"
},
{
"border": "normal",
"current_border_width": 3,
"floating": "auto_off",
"geometry": {
"height": 1030,
"width": 234,
"x": 0,
"y": 0
},
"name": "lsnes: Messages",
"percent": 0.5,
"swallows": [
{
"class": "^Lsnes$",
"title": "Messages"
}
],
"type": "con"
}
]
}
]
},
{
"border": "normal",
"floating": "auto_off",
"layout": "splitv",
"percent": 0.25,
"type": "con",
"nodes": [
{
"border": "normal",
"floating": "auto_off",
"layout": "tabbed",
"percent": 0.5,
"type": "con",
"nodes": [
{
"border": "normal",
"current_border_width": 3,
"floating": "auto_off",
"geometry": {
"height": 1030,
"width": 314,
"x": 0,
"y": 0
},
"name": "lsnes rr2-β24 [bsnes v085 (Compatibility core)]",
"percent": 0.5,
"swallows": [
{
"class": "^Lsnes$",
"title": "Compatibility"
}
],
"type": "con"
},
{
"border": "normal",
"current_border_width": 3,
"floating": "auto_off",
"geometry": {
"height": 1030,
"width": 208,
"x": 0,
"y": 0
},
"name": "lsnes: Messages",
"percent": 0.5,
"swallows": [
{
"class": "^Lsnes$",
"title": "Messages"
}
],
"type": "con"
}
]
},
{
"border": "normal",
"floating": "auto_off",
"layout": "tabbed",
"percent": 0.5,
"type": "con",
"nodes": [
{
"border": "normal",
"current_border_width": 3,
"floating": "auto_off",
"geometry": {
"height": 1030,
"width": 269,
"x": 0,
"y": 0
},
"name": "lsnes rr2-β24 [bsnes v085 (Compatibility core)]",
"percent": 0.5,
"swallows": [
{
"class": "^Lsnes$",
"title": "Compatibility"
}
],
"type": "con"
},
{
"border": "normal",
"current_border_width": 3,
"floating": "auto_off",
"geometry": {
"height": 1030,
"width": 154,
"x": 0,
"y": 0
},
"name": "lsnes: Messages",
"percent": 0.5,
"swallows": [
{
"class": "^Lsnes$",
"title": "Messages"
}
],
"type": "con"
}
]
}
]
},
{
"border": "normal",
"floating": "auto_off",
"layout": "splitv",
"percent": 0.25,
"type": "con",
"nodes": [
{
"border": "normal",
"floating": "auto_off",
"layout": "tabbed",
"percent": 0.5,
"type": "con",
"nodes": [
{
"border": "normal",
"current_border_width": 3,
"floating": "auto_off",
"geometry": {
"height": 1030,
"width": 186,
"x": 0,
"y": 0
},
"name": "lsnes rr2-β24 [bsnes v085 (Compatibility core)]",
"percent": 0.5,
"swallows": [
{
"class": "^Lsnes$",
"title": "Compatibility"
}
],
"type": "con"
},
{
"border": "normal",
"current_border_width": 3,
"floating": "auto_off",
"geometry": {
"height": 1030,
"width": 132,
"x": 0,
"y": 0
},
"name": "lsnes: Messages",
"percent": 0.5,
"swallows": [
{
"class": "^Lsnes$",
"title": "Messages"
}
],
"type": "con"
}
]
},
{
"border": "normal",
"floating": "auto_off",
"layout": "tabbed",
"percent": 0.5,
"type": "con",
"nodes": [
{
"border": "normal",
"current_border_width": 3,
"floating": "auto_off",
"geometry": {
"height": 1030,
"width": 168,
"x": 0,
"y": 0
},
"name": "lsnes rr2-β24 [bsnes v085 (Compatibility core)]",
"percent": 0.5,
"swallows": [
{
"class": "^Lsnes$",
"title": "Compatibility"
}
],
"type": "con"
},
{
"border": "normal",
"current_border_width": 3,
"floating": "auto_off",
"geometry": {
"height": 1030,
"width": 141,
"x": 0,
"y": 0
},
"name": "lsnes: Messages",
"percent": 0.5,
"swallows": [
{
"class": "^Lsnes$",
"title": "Messages"
}
],
"type": "con"
}
]
}
]
}
]
}
]

279
inflate-bit32.lua Normal file
View file

@ -0,0 +1,279 @@
-- zzlib-bit32 - zlib decompression in Lua - version using bit/bit32 libraries
-- Copyright (c) 2016-2020 Francois Galea <fgalea at free.fr>
-- This program is free software. It comes without any warranty, to
-- the extent permitted by applicable law. You can redistribute it
-- and/or modify it under the terms of the Do What The Fuck You Want
-- To Public License, Version 2, as published by Sam Hocevar. See
-- the COPYING file or http://www.wtfpl.net/ for more details.
local inflate = {}
local bit = bit32 or bit
inflate.band = bit.band
inflate.rshift = bit.rshift
function inflate.bitstream_init(file)
local bs = {
file = file, -- the open file handle
buf = nil, -- character buffer
len = nil, -- length of character buffer
pos = 1, -- position in char buffer
b = 0, -- bit buffer
n = 0, -- number of bits in buffer
}
-- get rid of n first bits
function bs:flushb(n)
self.n = self.n - n
self.b = bit.rshift(self.b,n)
end
-- peek a number of n bits from stream
function bs:peekb(n)
while self.n < n do
if self.pos > self.len then
self.buf = self.file:read(4096)
self.len = self.buf:len()
self.pos = 1
end
self.b = self.b + bit.lshift(self.buf:byte(self.pos),self.n)
self.pos = self.pos + 1
self.n = self.n + 8
end
return bit.band(self.b,bit.lshift(1,n)-1)
end
-- get a number of n bits from stream
function bs:getb(n)
local ret = bs:peekb(n)
self.n = self.n - n
self.b = bit.rshift(self.b,n)
return ret
end
-- get next variable-size of maximum size=n element from stream, according to Huffman table
function bs:getv(hufftable,n)
local e = hufftable[bs:peekb(n)]
local len = bit.band(e,15)
local ret = bit.rshift(e,4)
self.n = self.n - len
self.b = bit.rshift(self.b,len)
return ret
end
function bs:close()
if self.file then
self.file:close()
end
end
if type(file) == "string" then
bs.file = nil
bs.buf = file
else
bs.buf = file:read(4096)
end
bs.len = bs.buf:len()
return bs
end
local function hufftable_create(depths)
local nvalues = #depths
local nbits = 1
local bl_count = {}
local next_code = {}
for i=1,nvalues do
local d = depths[i]
if d > nbits then
nbits = d
end
bl_count[d] = (bl_count[d] or 0) + 1
end
local table = {}
local code = 0
bl_count[0] = 0
for i=1,nbits do
code = (code + (bl_count[i-1] or 0)) * 2
next_code[i] = code
end
for i=1,nvalues do
local len = depths[i] or 0
if len > 0 then
local e = (i-1)*16 + len
local code = next_code[len]
local rcode = 0
for j=1,len do
rcode = rcode + bit.lshift(bit.band(1,bit.rshift(code,j-1)),len-j)
end
for j=0,2^nbits-1,2^len do
table[j+rcode] = e
end
next_code[len] = next_code[len] + 1
end
end
return table,nbits
end
local function block_loop(out,bs,nlit,ndist,littable,disttable)
local lit
repeat
lit = bs:getv(littable,nlit)
if lit < 256 then
table.insert(out,lit)
elseif lit > 256 then
local nbits = 0
local size = 3
local dist = 1
if lit < 265 then
size = size + lit - 257
elseif lit < 285 then
nbits = bit.rshift(lit-261,2)
size = size + bit.lshift(bit.band(lit-261,3)+4,nbits)
else
size = 258
end
if nbits > 0 then
size = size + bs:getb(nbits)
end
local v = bs:getv(disttable,ndist)
if v < 4 then
dist = dist + v
else
nbits = bit.rshift(v-2,1)
dist = dist + bit.lshift(bit.band(v,1)+2,nbits)
dist = dist + bs:getb(nbits)
end
local p = #out-dist+1
while size > 0 do
table.insert(out,out[p])
p = p + 1
size = size - 1
end
end
until lit == 256
end
local function block_dynamic(out,bs)
local order = { 17, 18, 19, 1, 9, 8, 10, 7, 11, 6, 12, 5, 13, 4, 14, 3, 15, 2, 16 }
local hlit = 257 + bs:getb(5)
local hdist = 1 + bs:getb(5)
local hclen = 4 + bs:getb(4)
local depths = {}
for i=1,hclen do
local v = bs:getb(3)
depths[order[i]] = v
end
for i=hclen+1,19 do
depths[order[i]] = 0
end
local lengthtable,nlen = hufftable_create(depths)
local i=1
while i<=hlit+hdist do
local v = bs:getv(lengthtable,nlen)
if v < 16 then
depths[i] = v
i = i + 1
elseif v < 19 then
local nbt = {2,3,7}
local nb = nbt[v-15]
local c = 0
local n = 3 + bs:getb(nb)
if v == 16 then
c = depths[i-1]
elseif v == 18 then
n = n + 8
end
for j=1,n do
depths[i] = c
i = i + 1
end
else
error("wrong entry in depth table for literal/length alphabet: "..v);
end
end
local litdepths = {} for i=1,hlit do table.insert(litdepths,depths[i]) end
local littable,nlit = hufftable_create(litdepths)
local distdepths = {} for i=hlit+1,#depths do table.insert(distdepths,depths[i]) end
local disttable,ndist = hufftable_create(distdepths)
block_loop(out,bs,nlit,ndist,littable,disttable)
end
local function block_static(out,bs)
local cnt = { 144, 112, 24, 8 }
local dpt = { 8, 9, 7, 8 }
local depths = {}
for i=1,4 do
local d = dpt[i]
for j=1,cnt[i] do
table.insert(depths,d)
end
end
local littable,nlit = hufftable_create(depths)
depths = {}
for i=1,32 do
depths[i] = 5
end
local disttable,ndist = hufftable_create(depths)
block_loop(out,bs,nlit,ndist,littable,disttable)
end
local function block_uncompressed(out,bs)
bs:flushb(bit.band(bs.n,7))
local len = bs:getb(16)
if bs.n > 0 then
error("Unexpected.. should be zero remaining bits in buffer.")
end
local nlen = bs:getb(16)
if bit.bxor(len,nlen) ~= 65535 then
error("LEN and NLEN don't match")
end
for i=bs.pos,bs.pos+len-1 do
table.insert(out,bs.buf:byte(i,i))
end
bs.pos = bs.pos + len
end
function inflate.main(bs)
local last,type
local output = {}
repeat
local block
last = bs:getb(1)
type = bs:getb(2)
if type == 0 then
block_uncompressed(output,bs)
elseif type == 1 then
block_static(output,bs)
elseif type == 2 then
block_dynamic(output,bs)
else
error("unsupported block type")
end
until last == 1
bs:flushb(bit.band(bs.n,7))
return output
end
local crc32_table
function inflate.crc32(s,crc)
if not crc32_table then
crc32_table = {}
for i=0,255 do
local r=i
for j=1,8 do
r = bit.bxor(bit.rshift(r,1),bit.band(0xedb88320,bit.bnot(bit.band(r,1)-1)))
end
crc32_table[i] = r
end
end
crc = bit.bnot(crc or 0)
for i=1,#s do
local c = s:byte(i)
crc = bit.bxor(crc32_table[bit.bxor(c,bit.band(crc,0xff))],bit.rshift(crc,8))
end
crc = bit.bnot(crc)
if crc<0 then
-- in Lua < 5.2, sign extension was performed
crc = crc + 4294967296
end
return crc
end
return inflate

56
mem.lua Normal file
View file

@ -0,0 +1,56 @@
local _M = {
addr = {
kremcoins = 0x7e08cc,
tileCollisionMathPointer = 0x7e17b2,
spriteBase = 0x7e0de2,
verticalPointer = 0xc414,
tiledataPointer = 0x7e0098,
haveBoth = 0x7e08c2,
---Height in game units for vertical levels, width for horizontal
areaLength = 0x7e17b4,
---This is always the traditional width no matter the level type
areaWidth = 0x7e0afc,
---This is always the traditional height no matter the level type
areaHeight = 0x7e0afe,
cameraX = 0x7e17ba,
cameraY = 0x7e17c0,
cameraX2 = 0x7e0ad7,
cameraY2 = 0x7e0adb,
leadChar = 0x7e08a4,
partyX = 0x7e0a2a,
partyY = 0x7e0a2c,
solidLessThan = 0x7e00a0,
kongLetters = 0x7e0902,
mathLives = 0x7e08be,
displayLives = 0x7e0c0,
mainAreaNumber = 0x7e08a8,
currentAreaNumber = 0x7e08c8,
},
flag = {
sprite = {
dying = 0x1000,
}
},
size = {
tile = 32,
enemy = 64,
sprite = 94,
},
offset = {
sprite = {
control = 0x00,
x = 0x06,
y = 0x0a,
jumpHeight = 0x0e,
style = 0x12,
velocityX = 0x20,
velocityY = 0x24,
motion = 0x2e,
}
}
}
return _M

View file

@ -1,17 +1,33 @@
--Update to Seth-Bling's MarI/O app
local gui = gui
local base = string.gsub(@@LUA_SCRIPT_FILENAME@@, "(.*[/\\])(.*)", "%1")
local game = dofile(base.."/game.lua")
local config = dofile(base.."/config.lua")
local pool = dofile(base.."/pool.lua")
local util = dofile(base.."/util.lua")
local util = dofile(base.."/util.lua")()
local statusLine = nil
local statusColor = 0x0000ff00
pool.onMessage(function(msg, color)
print(msg)
local stderrColor = util.nearestColor(color, {
-- Green
['92'] = { r = 0 , g = 255, b = 0 },
-- Red
['91'] = { r = 255, g = 0 , b = 0 },
-- Yellow
['93'] = { r = 255, g = 255, b = 0 },
-- Blue
['94'] = { r = 0 , g = 0 , b = 255},
-- Magenta
['95'] = { r = 255, g = 0 , b = 255},
-- Cyan
['96'] = { r = 0 , g = 255, b = 255},
-- White
['97'] = { r = 255, g = 255, b = 255},
})
io.stderr:write('\x1b['..stderrColor..'m'..msg..'\x1b[0m\n')
statusLine = msg
statusColor = color
end)
@ -27,8 +43,22 @@ pool.onRenderForm(function(form)
form:draw(-500, 0)
if statusLine ~= nil then
gui.rectangle(-500, guiHeight - 20, 0, 20, 1, 0x00000000, statusColor)
gui.rectangle(-500, guiHeight - 20, guiWidth, 20, 1, 0x00000000, statusColor)
gui.text(-500, guiHeight - 20, statusLine, 0x00000000)
end
end)
pool.run()
local DONK_LOAD_POOL = os.getenv('DONK_LOAD_POOL')
if DONK_LOAD_POOL ~= nil and DONK_LOAD_POOL ~= "" then
pool.requestLoad(DONK_LOAD_POOL)
end
pool.run():next(function()
print("The pool finished running!!!")
end):catch(function(error)
if type(error) == "table" then
error = "\n"..table.concat(error, "\n")
end
io.stderr:write(string.format("There was a problem running the pool: %s", error))
print(string.format("There was a problem running the pool: %s", error))
end)

11
pipe-test.lua Normal file
View file

@ -0,0 +1,11 @@
local pipey = io.open("\\\\.\\pipe\\asoeuth", "r")
print('reader')
function on_timer()
print('read')
print(pipey:read("*l"))
set_timer_timeout(100000)
end
set_timer_timeout(100000)

323
pool.lua
View file

@ -1,16 +1,34 @@
local callback, set_timer_timeout, zip = callback, set_timer_timeout, zip
local base = string.gsub(@@LUA_SCRIPT_FILENAME@@, "(.*[/\\])(.*)", "%1")
local Promise = dofile(base.."/promise.lua")
local config = dofile(base.."/config.lua")
local util = dofile(base.."/util.lua")(Promise)
local serpent = dofile(base.."/serpent.lua")
local zzlib = dofile(base.."/zzlib.lua")
local hasThreads = config.NeatConfig.Threads > 1
-- Only the parent should manage ticks!
callback.register('timer', function()
Promise.update()
set_timer_timeout(1)
end)
set_timer_timeout(1)
local warn = '========== The ROM file to use comes from config.lua.'
io.stderr:write(warn)
print(warn)
local Runner = nil
if config.NeatConfig.Threads > 1 then
if hasThreads then
Runner = dofile(base.."/runner-wrapper.lua")
else
Runner = dofile(base.."/runner.lua")
end
local serpent = dofile(base.."/serpent.lua")
local libDeflate = dofile(base.."/LibDeflate.lua")
local Inputs = config.InputSize+1
local Outputs = #config.ButtonNames
@ -374,15 +392,15 @@ local function addToSpecies(child)
end
end
local function initializePool(after)
pool = newPool()
local function initializePool()
return util.promiseWrap(function()
pool = newPool()
for i=1,config.NeatConfig.Population do
basic = basicGenome()
addToSpecies(basic)
end
after()
for i=1,config.NeatConfig.Population do
local basic = basicGenome()
addToSpecies(basic)
end
end)
end
local function bytes(x)
@ -393,45 +411,51 @@ local function bytes(x)
return string.char(b1,b2,b3,b4)
end
--- Saves the pool to a gzipped Serpent file
---@param filename string Filename to write
---@return Promise Promise A promise that resolves when the file is saved.
local function writeFile(filename)
local file = io.open(filename, "w")
local dump = serpent.dump(pool)
local zlib = libDeflate:CompressDeflate(dump)
file:write("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\x00")
file:write(zlib)
file:write(string.char(0,0,0,0))
file:write(bytes(#dump % (2^32)))
file:close()
return
return util.promiseWrap(function ()
local file = zip.writer.new(filename)
file:create_file('data.serpent')
local dump = serpent.dump(pool)
file:write(dump)
file:close_file()
file:commit()
end)
end
-- FIXME Save/load mechanism has to be rethought with items running in parallel
local function loadFile(filename, after)
message("Loading pool from " .. filename, 0x00999900)
local file = io.open(filename, "r")
if file == nil then
message("File could not be loaded", 0x00990000)
return
end
local contents = file:read("*all")
local obj, err = loadstring(libDeflate:DecompressDeflate(contents:sub(11, #contents - 8)))
if err ~= nil then
message(string.format("Error parsing: %s", err), 0x00990000)
return
end
-- FIXME This isn't technically asynchronous. Probably can't be though.
local function loadFile(filename)
return util.promiseWrap(function()
message("Loading pool from " .. filename, 0x00999900)
local file = io.open(filename, "r")
if file == nil then
message("File could not be loaded", 0x00990000)
return
end
local contents = file:read("*all")
file:close()
local decomp = zzlib.unzip(contents, 'data.serpent')
local ok, obj = serpent.load(decomp)
if not ok then
message("Error parsing pool file", 0x00990000)
return
end
pool = obj()
pool = obj
end)
end
local function savePool()
local filename = _M.saveLoadFile
writeFile(filename)
message(string.format("Saved \"%s\"!", filename:sub(#filename - 50)), 0x00009900)
return writeFile(filename):next(function()
message(string.format("Saved \"%s\"!", filename:sub(#filename - 50)), 0x00009900)
end)
end
local function loadPool(after)
loadFile(_M.saveLoadFile, after)
after()
local function loadPool()
return loadFile(_M.saveLoadFile)
end
local function processRenderForm(form)
@ -459,7 +483,7 @@ end
local function crossover(g1, g2)
-- Make sure g1 is the higher fitness genome
if g2.fitness > g1.fitness then
tempg = g1
local tempg = g1
g1 = g2
g2 = tempg
end
@ -550,11 +574,11 @@ end
local function breedChild(species)
local child = {}
if math.random() < config.NeatConfig.CrossoverChance then
g1 = species.genomes[math.random(1, #species.genomes)]
g2 = species.genomes[math.random(1, #species.genomes)]
local g1 = species.genomes[math.random(1, #species.genomes)]
local g2 = species.genomes[math.random(1, #species.genomes)]
child = crossover(g1, g2)
else
g = species.genomes[math.random(1, #species.genomes)]
local g = species.genomes[math.random(1, #species.genomes)]
child = copyGenome(g)
end
@ -593,7 +617,7 @@ local function removeWeakSpecies()
local sum = totalAverageFitness()
for s = 1,#pool.species do
local species = pool.species[s]
breed = math.floor(species.averageFitness / sum * config.NeatConfig.Population)
local breed = math.floor(species.averageFitness / sum * config.NeatConfig.Population)
if breed >= 1 then
table.insert(survived, species)
end
@ -616,7 +640,7 @@ local function newGeneration()
local children = {}
for s = 1,#pool.species do
local species = pool.species[s]
breed = math.floor(species.averageFitness / sum * config.NeatConfig.Population) - 1
local breed = math.floor(species.averageFitness / sum * config.NeatConfig.Population) - 1
for i=1,breed do
table.insert(children, breedChild(species))
end
@ -637,89 +661,108 @@ local function newGeneration()
pool.generation = pool.generation + 1
writeFile(_M.saveLoadFile .. ".gen" .. pool.generation .. ".pool")
return writeFile(_M.saveLoadFile .. ".gen" .. pool.generation .. ".pool"):next(function()
if config.NeatConfig.AutoSave then
return writeFile(_M.saveLoadFile)
end
end)
end
local loadRequested = false
local saveRequested = false
local function mainLoop(currentSpecies)
if loadRequested then
loadRequested = false
loadPool(mainLoop)
return
end
if saveRequested then
saveRequested = false
savePool()
end
if topRequested then
topRequested = false
playTop()
return
end
if not config.Running then
-- FIXME Tick?
end
if currentSpecies == nil then
currentSpecies = 1
end
local runner = Runner()
runner.onMessage(function(msg, color)
message(msg, color)
end)
runner.onSave(function(filename)
_M.requestSave(filename)
end)
runner.onLoad(function(filename)
_M.requestLoad(filename)
end)
runner.onRenderForm(function(form)
processRenderForm(form)
end)
local slice = pool.species[currentSpecies]
if config.NeatConfig.Threads > 1 then
slice = {}
for i=currentSpecies, currentSpecies + config.NeatConfig.Threads - 1, 1 do
if pool.species[i] == nil then
break
end
table.insert(slice, pool.species[i])
end
end
local finished = 0
runner.run(
slice,
pool.generation,
function()
-- Genome callback
end,
function()
if config.NeatConfig.Threads > 1 then
finished = finished + 1
if finished ~= #slice then
return
end
currentSpecies = currentSpecies + #slice
else
currentSpecies = currentSpecies + 1
end
if currentSpecies > #pool.species then
newGeneration()
currentSpecies = 1
end
mainLoop(currentSpecies)
end
)
local function reset()
return _M.run(true)
end
local runner = Runner(Promise)
runner.onMessage(function(msg, color)
message(msg, color)
end)
runner.onSave(function(filename)
_M.requestSave(filename)
end)
runner.onLoad(function(filename)
_M.requestLoad(filename)
end)
runner.onReset(function(filename)
_M.requestReset()
end)
runner.onRenderForm(function(form)
processRenderForm(form)
end)
local playTop = nil
local topRequested = false
local function playTop()
local loadRequested = config.NeatConfig.AutoSave
local saveRequested = false
local resetRequested = false
local function mainLoop(currentSpecies, topGenome)
if currentSpecies == nil then
currentSpecies = 1
end
local slice = pool.species[currentSpecies]
return util.promiseWrap(function()
if loadRequested then
loadRequested = false
currentSpecies = nil
-- FIXME
return loadPool()
end
if saveRequested then
saveRequested = false
return savePool()
end
if resetRequested then
resetRequested = false
return reset()
end
if topRequested then
topRequested = false
return playTop()
end
if not config.Running then
-- FIXME Tick?
end
if hasThreads then
slice = pool.species
end
return runner.run(
slice,
pool.generation,
function()
-- Genome callback
-- FIXME Should we do something here??? What was your plan, past me?
end
):next(function(maxFitness)
if maxFitness > pool.maxFitness then
pool.maxFitness = maxFitness
end
if hasThreads then
currentSpecies = currentSpecies + #slice
else
currentSpecies = currentSpecies + 1
end
if currentSpecies > #pool.species then
newGeneration()
currentSpecies = 1
end
end)
end):next(function ()
if topGenome == nil then
return mainLoop(currentSpecies)
end
end)
end
playTop = function()
local maxfitness = 0
local maxs, maxg
for s,species in pairs(pool.species) do
@ -733,7 +776,7 @@ local function playTop()
end
-- FIXME genome
mainLoop(maxs)
return mainLoop(maxs, maxg)
end
function _M.requestLoad(filename)
@ -746,6 +789,10 @@ function _M.requestSave(filename)
saveRequested = true
end
function _M.requestReset()
resetRequested = true
end
function _M.onMessage(handler)
table.insert(_M.onMessageHandler, handler)
end
@ -759,15 +806,23 @@ function _M.requestTop()
end
function _M.run(reset)
local promise = nil
if pool == nil or reset == true then
initializePool(function()
writeFile(config.PoolDir.."temp.pool")
mainLoop()
end)
else
writeFile(config.PoolDir.."temp.pool")
mainLoop()
promise = initializePool()
else
promise = Promise.new()
promise:resolve()
end
return promise:next(function()
return writeFile(config.PoolDir.."temp.pool")
end):next(function ()
if not hasThreads then
return util.loadAndStart(config.ROM)
end
end):next(function()
return mainLoop()
end)
end
return _M
return _M

BIN
pool/MainbraceMayhem.lsmv Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

298
promise.lua Normal file
View file

@ -0,0 +1,298 @@
--[[
The MIT License (MIT)
=====================
Copyright © `2015` `Colin Fein`
Permission is hereby granted, free of charge, to any person
obtaining a copy of this software and associated documentation
files (the Software), to deal in the Software without
restriction, including without limitation the rights to use,
copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the
Software is furnished to do so, subject to the following
conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
OTHER DEALINGS IN THE SOFTWARE.
]]
-- Port of https://github.com/rhysbrettbowen/promise_impl/blob/master/promise.js
-- and https://github.com/rhysbrettbowen/Aplus
--
local queue = {}
local State = {
PENDING = 'pending',
FULFILLED = 'fulfilled',
REJECTED = 'rejected',
}
local passthrough = function(x) return x end
local errorthrough = function(x) error(x) end
local function callable_table(callback)
local mt = getmetatable(callback)
return type(mt) == 'table' and type(mt.__call) == 'function'
end
local function is_callable(value)
local t = type(value)
return t == 'function' or (t == 'table' and callable_table(value))
end
local transition, resolve, run
local Promise = {
is_promise = true,
state = State.PENDING
}
Promise.mt = { __index = Promise }
local do_async = function(callback)
if Promise.async then
Promise.async(callback)
else
table.insert(queue, callback)
end
end
local reject = function(promise, reason)
transition(promise, State.REJECTED, reason)
end
local fulfill = function(promise, value)
transition(promise, State.FULFILLED, value)
end
transition = function(promise, state, value)
if promise.state == state
or promise.state ~= State.PENDING
or ( state ~= State.FULFILLED and state ~= State.REJECTED )
then
return
end
promise.state = state
promise.value = value
run(promise)
end
function Promise:next(on_fulfilled, on_rejected)
local promise = Promise.new()
table.insert(self.queue, {
fulfill = is_callable(on_fulfilled) and on_fulfilled or nil,
reject = is_callable(on_rejected) and on_rejected or nil,
promise = promise
})
run(self)
return promise
end
resolve = function(promise, x)
if promise == x then
reject(promise, 'TypeError: cannot resolve a promise with itself')
return
end
local x_type = type(x)
if x_type ~= 'table' then
fulfill(promise, x)
return
end
-- x is a promise in the current implementation
if x.is_promise then
-- 2.3.2.1 if x is pending, resolve or reject this promise after completion
if x.state == State.PENDING then
x:next(
function(value)
resolve(promise, value)
end,
function(reason)
reject(promise, reason)
end
)
return
end
-- if x is not pending, transition promise to x's state and value
transition(promise, x.state, x.value)
return
end
local called = false
-- 2.3.3.1. Catches errors thrown by __index metatable
local success, reason = pcall(function()
local next = x.next
if is_callable(next) then
next(
x,
function(y)
if not called then
resolve(promise, y)
called = true
end
end,
function(r)
if not called then
reject(promise, r)
called = true
end
end
)
else
fulfill(promise, x)
end
end)
if not success then
if not called then
reject(promise, reason)
end
end
end
run = function(promise)
if promise.state == State.PENDING then return end
do_async(function()
-- drain promise.queue while allowing pushes from within callbacks
local q = promise.queue
local i = 0
while i < #q do
i = i + 1
local obj = q[i]
local success, result = pcall(function()
local success = obj.fulfill or passthrough
local failure = obj.reject or errorthrough
local callback = promise.state == State.FULFILLED and success or failure
return callback(promise.value)
end)
if not success then
reject(obj.promise, result)
else
resolve(obj.promise, result)
end
end
for j = 1, i do
q[j] = nil
end
end)
end
function Promise.new(callback)
local instance = {
queue = {}
}
setmetatable(instance, Promise.mt)
if callback then
callback(
function(value)
resolve(instance, value)
end,
function(reason)
reject(instance, reason)
end
)
end
return instance
end
function Promise:catch(callback)
return self:next(nil, callback)
end
function Promise:resolve(value)
fulfill(self, value)
end
function Promise:reject(reason)
reject(self, reason)
end
function Promise.update()
while true do
local async = table.remove(queue, 1)
if not async then
break
end
async()
end
end
-- resolve when all promises complete
function Promise.all(...)
local promises = {...}
local results = {}
local state = State.FULFILLED
local remaining = #promises
local promise = Promise.new()
local check_finished = function()
if remaining > 0 then
return
end
transition(promise, state, results)
end
for i,p in ipairs(promises) do
p:next(
function(value)
results[i] = value
remaining = remaining - 1
check_finished()
end,
function(value)
results[i] = value
remaining = remaining - 1
state = State.REJECTED
check_finished()
end
)
end
check_finished()
return promise
end
-- resolve with first promise to complete
function Promise.race(...)
local promises = {...}
local promise = Promise.new()
Promise.all(...):next(nil, function(value)
reject(promise, value)
end)
local success = function(value)
fulfill(promise, value)
end
for _,p in ipairs(promises) do
p:next(success)
end
return promise
end
return Promise

View file

@ -1,55 +1,69 @@
local gui, utime, callback, set_timer_timeout = gui, utime, callback, set_timer_timeout
local base = string.gsub(@@LUA_SCRIPT_FILENAME@@, "(.*[/\\])(.*)", "%1")
local Promise = dofile(base.."/promise.lua")
-- Only the parent should manage ticks!
callback.register('timer', function()
Promise.update()
set_timer_timeout(1)
end)
set_timer_timeout(1)
local Runner = dofile(base.."/runner.lua")
local serpent = dofile(base.."/serpent.lua")
local util = dofile(base.."/util.lua")
local runnerDataFile = io.open(os.getenv("RUNNER_DATA"), 'r')
local runnerData, err = loadstring(runnerDataFile:read('*a'))
runnerDataFile:close()
if err ~= nil then
print(err)
return
end
runnerData = runnerData()
local species = runnerData[1]
local speciesId = species.id
local generationIndex = runnerData[2]
local filename = runnerData[3]
local outFile = io.open(filename, "w")
local outContents = {}
local util = dofile(base.."/util.lua")(Promise)
local statusLine = nil
local statusColor = 0x0000ff00
local runner = Runner()
local species = nil
local speciesId = -1
local generationIndex = nil
local inputPipeName = os.getenv("RUNNER_INPUT_PIPE")
local outputPipeName = os.getenv("RUNNER_OUTPUT_PIPE")
print('Opening input pipe '..inputPipeName)
local inputPipe = util.openReadPipe(inputPipeName)
if inputPipe == nil then
error('Error opening input file')
end
print('Opened input pipe '..inputPipeName)
print('Opening output file '..outputPipeName)
local outputPipe = util.openReadPipeWriter(outputPipeName)
print('Opened output file '..outputPipeName)
local function writeResponse(object)
outputPipe:write(serpent.dump(object).."\n")
outputPipe:flush()
end
local function unblockLoop()
return util.delay(1000000):next(function()
outputPipe:write(".\n")
outputPipe:flush()
return unblockLoop()
end)
end
local runner = Runner(Promise)
runner.onMessage(function(msg, color)
statusLine = msg
statusColor = color
print(msg)
table.insert(
outContents,
serpent.dump({
type = 'onMessage',
speciesId = speciesId,
msg = msg,
color = color,
})
)
writeResponse({
type = 'onMessage',
speciesId = speciesId,
msg = msg,
color = color,
})
end)
local guiHeight = 0
local guiWidth = 0
runner.onRenderForm(function(form)
guiWidth, guiHeight = gui.resolution()
local guiWidth, guiHeight = gui.resolution()
gui.left_gap(0)
gui.top_gap(0)
gui.bottom_gap(0)
@ -57,7 +71,7 @@ runner.onRenderForm(function(form)
form:draw(0, 0)
if statusLine ~= nil then
gui.rectangle(0, guiHeight - 20, 0, 20, 1, 0x00000000, statusColor)
gui.rectangle(0, guiHeight - 20, guiWidth, 20, 1, 0x00000000, statusColor)
gui.text(0, guiHeight - 20, statusLine, 0x00000000)
end
@ -65,51 +79,88 @@ runner.onRenderForm(function(form)
end)
runner.onSave(function(filename)
table.insert(
outContents,
serpent.dump({
type = 'onSave',
filename = filename,
speciesId = speciesId,
})
)
writeResponse({
type = 'onSave',
filename = filename,
speciesId = speciesId,
})
end)
runner.onLoad(function(filename)
table.insert(
outContents,
serpent.dump({
type = 'onLoad',
filename = filename,
speciesId = speciesId,
})
)
writeResponse({
type = 'onLoad',
filename = filename,
speciesId = speciesId,
})
end)
runner.run(
species,
generationIndex,
function(genome, index)
table.insert(
outContents,
serpent.dump({
type = 'onGenome',
genome = genome,
genomeIndex = index,
speciesId = speciesId,
})
)
end,
function()
table.insert(
outContents,
serpent.dump({
runner.onReset(function()
writeResponse({
type = 'onReset',
speciesId = speciesId,
})
end)
local function waitLoop(inputLine)
return util.promiseWrap(function()
local ok, inputData = serpent.load(inputLine)
if not ok or inputData == nil then
io.stderr:write("Deserialization error\n")
io.stderr:write(inputLine.."\n")
return
end
print('Received input from master process')
species = inputData[1]
speciesId = species.id
generationIndex = inputData[2]
print('Running')
return runner.run(
species,
generationIndex,
function(genome, index)
writeResponse({
type = 'onGenome',
genome = genome,
genomeIndex = index,
speciesId = speciesId,
})
end
):next(function(maxFitness)
writeResponse({
type = 'onFinish',
maxFitness = maxFitness,
speciesId = speciesId,
})
)
outFile:write(table.concat(outContents, "\n"))
outFile:close()
exec('quit-emulator')
end)
end):next(function()
return inputPipe:read("*l")
end):next(waitLoop)
end
local sec, usec = utime()
local ts = sec * 1000000 + usec
local waiter = util.promiseWrap(function()
return inputPipe:read("*l")
end)
writeResponse({ type = 'onInit', ts = ts })
print(string.format('Wrote init to output at %d', ts))
waiter:next(function(inputLine)
return waitLoop(inputLine)
end):catch(function(error)
if type(error) == "table" then
error = "\n"..table.concat(error, "\n")
end
)
print('Runner process error: '..error)
io.stderr:write('Runner process error: '..error..'\n')
end)

View file

@ -1,8 +1,19 @@
local random = random
local base = string.gsub(@@LUA_SCRIPT_FILENAME@@, "(.*[/\\])(.*)", "%1")
local Promise = nil
local util = nil
local config = dofile(base.."/config.lua")
local serpent = dofile(base.."/serpent.lua")
local tmpFileName = "/tmp/donk_runner_"..tostring(math.floor(random.integer(0, 0xffffffffffffffff))):hex()
local pipePrefix = "donk_runner_"..
string.hex(math.floor(random.integer(0, 0xffffffff)))..
string.hex(math.floor(random.integer(0, 0xffffffff)))
local inputPrefix = pipePrefix..'_input_'
local outputPrefix = pipePrefix..'_output_'
local function message(_M, msg, color)
if color == nil then
@ -34,17 +45,96 @@ local function onLoad(_M, handler)
table.insert(_M.onLoadHandler, handler)
end
local function reset(_M)
for i=#_M.onResetHandler,1,-1 do
_M.onResetHandler[i]()
end
end
local function onReset(_M, handler)
table.insert(_M.onResetHandler, handler)
end
local function onMessage(_M, handler)
table.insert(_M.onMessageHandler, handler)
end
return function()
--- Launches the child processes
---@param _M table The instance
---@param count integer Number of processes needed
---@return Promise Promise A promise that resolves when all the processes are ready
local function launchChildren(_M, count)
local promises = {}
for i=#_M.poppets+1,count,1 do
local newOne = {
process = nil,
output = util.openReadPipe(outputPrefix..i),
input = nil,
}
local outputPipeName = outputPrefix..i
local inputPipeName = inputPrefix..i
local settingsDir = nil
if util.isWin then
settingsDir = util.getTempDir().."/donk_runner_settings_"..i
util.mkdir(settingsDir)
end
local envs = {
RUNNER_INPUT_PIPE = inputPipeName,
RUNNER_OUTPUT_PIPE = outputPipeName,
APPDATA = settingsDir,
}
local cmd = '"'.._M.hostProcess..'" "--rom='..config.ROM..'" --unpause "--lua='..base..'/runner-process.lua"'
newOne.process = util.popenCmd(cmd, nil, envs)
-- Wait for init
local promise = util.promiseWrap(function()
newOne.output:read("*l")
while newOne.input == nil do
newOne.input = util.openReadPipeWriter(inputPipeName)
end
end)
table.insert(promises, promise)
table.insert(_M.poppets, newOne)
end
return Promise.all(table.unpack(promises))
end
return function(promise)
-- FIXME Should this be a global???
Promise = promise
if util == nil then
util = dofile(base.."/util.lua")(Promise)
end
-- FIXME Maybe don't do this in the "constructor"?
if util.isWin then
util.downloadFile("https://github.com/psmay/windows-named-pipe-utils/releases/download/v0.1.1/build.zip", base.."/namedpipe.zip")
util.unzip(base.."/namedpipe.zip", base)
os.rename(base.."/build", "namedpipe")
end
local _M = {
onMessageHandler = {},
onResetHandler = {},
onSaveHandler = {},
onLoadHandler = {},
poppets = {},
hostProcess = "lsnes",
}
if util.isWin then
_M.hostProcess = util.scrapeCmd('*l', 'powershell "(Get-WmiObject Win32_Process -Filter ProcessId=$((Get-WmiObject Win32_Process -Filter ProcessId=$((Get-WmiObject Win32_Process -Filter ProcessId=$PID).ParentProcessId)).ParentProcessId)").ExecutablePath')
if _M.hostProcess == nil or _M.hostProcess == "" then
_M.hostProcess = "lsnes-bsnes.exe"
end
else
-- FIXME Linux
end
_M.onRenderForm = function(handler)
end
@ -64,68 +154,110 @@ return function()
onLoad(_M, handler)
end
_M.run = function(species, generationIdx, genomeCallback, finishCallback)
local poppets = {}
for i=1,#species,1 do
local outputFileName = tmpFileName..'_output_'..i
_M.onReset = function(handler)
onReset(_M, handler)
end
local inputFileName = tmpFileName.."_input_"..i
local inputFile = io.open(inputFileName, 'w')
inputFile:write(serpent.dump({species[i], generationIdx, outputFileName}))
inputFile:close()
local cmd = "RUNNER_DATA=\""..inputFileName.."\" lsnes \"--rom="..config.ROM.."\" --unpause \"--lua="..base.."/runner-process.lua\""
message(_M, cmd)
local poppet = io.popen(cmd, 'r')
table.insert(poppets, poppet)
end
_M.run = function(species, generationIdx, genomeCallback)
local promise = Promise.new()
promise:resolve()
return promise:next(function()
return launchChildren(_M, config.NeatConfig.Threads)
end):next(function()
message(_M, 'Setting up child processes')
for i=1,#poppets,1 do
local poppet = poppets[i]
poppet:read('*a')
poppet:close()
end
for i=1,#species,1 do
local outputFileName = tmpFileName..'_output_'..i
local outputFile = io.open(outputFileName, "r")
local line = ""
repeat
local obj, err = loadstring(line)
if err ~= nil then
goto continue
end
obj = obj()
if obj == nil then
goto continue
end
if obj.type == 'onMessage' then
message(_M, obj.msg, obj.color)
elseif obj.type == 'onLoad' then
load(_M, obj.filename)
elseif obj.type == 'onSave' then
save(_M, obj.filename)
elseif obj.type == 'onGenome' then
for i=1,#species,1 do
local s = species[i]
if s.id == obj.speciesId then
s.genomes[obj.genomeIndex] = obj.genome
break
end
local maxFitness = nil
local function readLoop(outputPipe)
return util.promiseWrap(function()
return outputPipe:read("*l")
end):next(function(line)
if line == nil or line == "" then
util.closeCmd(outputPipe)
end
genomeCallback(obj.genome, obj.index)
elseif obj.type == 'onFinish' then
finishCallback()
end
::continue::
line = outputFile:read()
until(line == "" or line == nil)
outputFile:close()
end
local ok, obj = serpent.load(line)
if not ok then
return false
end
if obj == nil then
return false
end
if obj.type == 'onMessage' then
message(_M, obj.msg, obj.color)
elseif obj.type == 'onLoad' then
load(_M, obj.filename)
elseif obj.type == 'onSave' then
save(_M, obj.filename)
elseif obj.type == 'onReset' then
reset(_M)
elseif obj.type == 'onGenome' then
for i=1,#species,1 do
local s = species[i]
if s.id == obj.speciesId then
message(_M, string.format('Write Species %d Genome %d', obj.speciesId, obj.genomeIndex))
s.genomes[obj.genomeIndex] = obj.genome
break
end
end
genomeCallback(obj.genome, obj.index)
elseif obj.type == 'onFinish' then
if maxFitness == nil or obj.maxFitness > maxFitness then
maxFitness = obj.maxFitness
end
return true
end
end):next(function(finished)
if finished then
return maxFitness
end
return readLoop(outputPipe)
end)
end
local waiters = {}
for t=1,config.NeatConfig.Threads,1 do
waiters[t] = Promise.new()
waiters[t]:resolve()
end
local currentSpecies = 1
while currentSpecies < #species do
for t=1,config.NeatConfig.Threads,1 do
local s = species[currentSpecies]
if s == nil then
break
end
local inputPipe = _M.poppets[t].input
local outputPipe = _M.poppets[t].output
waiters[t] = waiters[t]:next(function()
inputPipe:write(serpent.dump({s, generationIdx}).."\n")
inputPipe:flush()
return readLoop(outputPipe)
end)
currentSpecies = currentSpecies + 1
end
end
message(_M, 'Waiting for child processes to finish')
return Promise.all(table.unpack(waiters))
end):next(function(maxFitnesses)
message(_M, 'Child processes finished')
local maxestFitness = maxFitnesses[1]
for i=1,#maxFitnesses,1 do
local maxFitness = maxFitnesses[i]
if maxFitness > maxestFitness then
maxestFitness = maxFitness
end
end
return maxestFitness
end)
end
return _M

View file

@ -1,8 +1,13 @@
local mem = require "mem"
local gui, input, movie, settings, exec, callback, set_timer_timeout, memory, bsnes = gui, input, movie, settings, exec, callback, set_timer_timeout, memory, bsnes
local base = string.gsub(@@LUA_SCRIPT_FILENAME@@, "(.*[/\\])(.*)", "%1")
local Promise = nil
local config = dofile(base.."/config.lua")
local game = dofile(base.."/game.lua")
local game = nil
local mathFunctions = dofile(base.."/mathFunctions.lua")
local util = dofile(base.."/util.lua")()
local Inputs = config.InputSize+1
local Outputs = #config.ButtonNames
@ -81,6 +86,18 @@ local function displayGenome(genome)
if gene.enabled then
local c1 = cells[gene.into]
local c2 = cells[gene.out]
if c1 == nil then
c1 = {
x = 0,
y = 0,
}
end
if c2 == nil then
c2 = {
x = 0,
y = 0,
}
end
if gene.into > Inputs and gene.into <= config.NeatConfig.MaxNodes then
c1.x = 0.75*c1.x + 0.25*c2.x
if c1.x >= c2.x then
@ -147,6 +164,18 @@ local function displayGenome(genome)
if true then
local c1 = cells[gene.into]
local c2 = cells[gene.out]
if(c1 == nil) then
c1 = {
x = 0,
y = 0,
}
end
if(c2 == nil) then
c2 = {
x = 0,
y = 0,
}
end
local alpha = 0x20000000
if c1.value == 0 then
alpha = 0xA0000000
@ -187,19 +216,9 @@ local function displayGenome(genome)
gui.renderctx.setnull()
end
local function advanceFrame(_M, after)
table.insert(_M.onFrameAdvancedHandler, after)
end
local function processFrameAdvanced(_M)
for i=#_M.onFrameAdvancedHandler,1,-1 do
table.remove(_M.onFrameAdvancedHandler, i)()
end
end
local buttons = nil
local buttonCtx = gui.renderctx.new(500, 70)
function displayButtons()
local function displayButtons(_M)
buttonCtx:set()
buttonCtx:clear()
@ -213,7 +232,7 @@ function displayButtons()
end
gui.text(5, 2, "[1] "..startStop)
gui.text(130, 2, "[4] Play Top")
--gui.text(130, 2, "[4] Play Top")
gui.text(240, 2, "[6] Save")
@ -223,7 +242,7 @@ function displayButtons()
local insert = ""
local confirm = "[Tab] Type in filename"
if inputmode then
if _M.inputmode then
insert = "_"
confirm = "[Tab] Confirm filename"
end
@ -236,15 +255,26 @@ function displayButtons()
gui.renderctx.setnull()
end
local frame = 0
local function getDistanceTraversed(areaInfos)
local distanceTraversed = 0
for _,areaInfo in pairs(areaInfos) do
for i=1,#areaInfo.waypoints,1 do
local waypoint = areaInfo.waypoints[i]
distanceTraversed = distanceTraversed + (waypoint.startDistance - waypoint.shortest)
end
end
return distanceTraversed
end
local formCtx = nil
local form = nil
function displayForm(_M)
if #_M.onRenderFormHandler == 0 then
local function displayForm(_M)
if config.NeatConfig.ShowInterface == false or #_M.onRenderFormHandler == 0 then
return
end
if form ~= nil and frame % 10 ~= 0 then
if form ~= nil and _M.drawFrame % 10 ~= 0 then
gui.renderctx.setnull()
for i=#_M.onRenderFormHandler,1,-1 do
_M.onRenderFormHandler[i](form)
end
@ -255,25 +285,33 @@ function displayForm(_M)
formCtx:clear()
gui.rectangle(0, 0, 500, guiHeight, 1, 0x00ffffff, 0xbb000000)
--gui.circle(game.screenX-84, game.screenY-84, 192 / 2, 1, 0x50000000)
local distanceTraversed = getDistanceTraversed(_M.areaInfo)
local goalX = 0
local goalY = 0
local areaInfo = _M.areaInfo[_M.currentArea]
if areaInfo ~= nil then
goalX = areaInfo.preferredExit.x
goalY = areaInfo.preferredExit.y
end
gui.text(5, 30, "Timeout: " .. _M.timeout)
gui.text(5, 5, "Generation: " .. _M.currentGenerationIndex)
gui.text(130, 5, "Species: " .. _M.currentSpecies.id)
gui.text(230, 5, "Genome: " .. _M.currentGenomeIndex)
gui.text(130, 30, "Max: " .. math.floor(_M.maxFitness))
--gui.text(330, 5, "Measured: " .. math.floor(measured/total*100) .. "%")
gui.text(5, 65, "Bananas: " .. (game.getBananas() - _M.startBananas))
gui.text(5, 80, "KONG: " .. (game.getKong() - _M.startKong))
gui.text(5, 95, "Krem: " .. (game.getKremCoins() - _M.startKrem))
gui.text(130, 65, "Coins: " .. (game.getCoins() - _M.startCoins))
gui.text(130, 80, "Lives: " .. game.getLives())
gui.text(130, 95, "Bumps: " .. _M.bumps)
gui.text(230, 65, "Damage: " .. _M.partyHitCounter)
gui.text(230, 80, "PowerUp: " .. _M.powerUpCounter)
gui.text(320, 65, string.format("Current Area: %04x", _M.currentArea))
gui.text(320, 80, "Rightmost: ".._M.rightmost[_M.currentArea])
gui.text(5, 5, string.format([[
Generation: %4d Species: %4d Genome: %4d
displayButtons()
Timeout: %4d Max: %6d
Bananas: %4d Coins: %3d Damage: %3d Current area: %04x
KONG: %7d Lives: %3d Powerup: %2d Traveled: %8d
Krem: %7d Bumps: %3d Goal Offset: %8d, %7d
]],
_M.currentGenerationIndex, _M.currentSpecies.id, _M.currentGenomeIndex,
_M.timeout, math.floor(_M.maxFitness),
_M.totalBananas, game.getCoins() - _M.startCoins, _M.partyHitCounter, _M.currentArea,
game.getKong() - _M.startKong, game.getLives(), _M.powerUpCounter, distanceTraversed,
game.getKremCoins() - _M.startKrem, _M.bumps, goalX - game.partyX, goalY - game.partyY
))
displayButtons(_M)
formCtx:set()
buttons:draw(5, 130)
@ -294,18 +332,17 @@ local function painting(_M)
if formCtx == nil then
formCtx = gui.renderctx.new(500, guiHeight)
end
frame = frame + 1
_M.drawFrame = _M.drawFrame + 1
displayForm(_M)
end
local function evaluateNetwork(network, inputs, inputDeltas)
local function evaluateNetwork(_M, network, inputs, inputDeltas)
table.insert(inputs, 1)
table.insert(inputDeltas,99)
if #inputs ~= Inputs then
message(_M, "Incorrect number of neural network inputs.", 0x00990000)
return {}
end
for i=1,Inputs do
network.neurons[i].value = inputs[i] * inputDeltas[i]
@ -343,23 +380,8 @@ local function evaluateNetwork(network, inputs, inputDeltas)
return outputs
end
local function evaluateCurrent(_M)
local genome = _M.currentSpecies.genomes[_M.currentGenomeIndex]
local inputDeltas = {}
inputs, inputDeltas = game.getInputs()
controller = evaluateNetwork(genome.network, inputs, inputDeltas)
if controller[6] and controller[7] then
controller[6] = false
controller[7] = false
end
if controller[4] and controller[5] then
controller[4] = false
controller[5] = false
end
local controller = {}
local function updateController()
for b=0,#config.ButtonNames - 1,1 do
if controller[b] then
input.set(0, b, 1)
@ -369,221 +391,30 @@ local function evaluateCurrent(_M)
end
end
local frame = 0
local lastFrame = 0
local function evaluateCurrent(_M, inputs, inputDeltas)
local genome = _M.currentSpecies.genomes[_M.currentGenomeIndex]
controller = evaluateNetwork(_M, genome.network, inputs, inputDeltas)
if controller[6] and controller[7] then
controller[6] = false
controller[7] = false
end
if controller[4] and controller[5] then
controller[4] = false
controller[5] = false
end
end
local function fitnessAlreadyMeasured(_M)
local genome = _M.currentSpecies.genomes[_M.currentGenomeIndex]
return genome.fitness ~= 0
end
local rew = movie.to_rewind(config.NeatConfig.Filename)
local function initializeRun(_M, after)
message(_M, string.format("Total Genomes: %d", #_M.currentSpecies.genomes))
settings.set_speed("turbo")
-- XXX Does this actually work or only affects new VM loads?
settings.set('lua-maxmem', 1024)
exec('enable-sound off')
gui.subframe_update(false)
table.insert(_M.runInitialized, after)
movie.unsafe_rewind(rew)
end
local function mainLoop(_M, genome)
advanceFrame(_M, function()
if genome ~= nil then
_M.currentFrame = _M.currentFrame + 1
end
genome = _M.currentSpecies.genomes[_M.currentGenomeIndex]
if frame % 10 == 0 then
if not pcall(function()
displayGenome(genome)
end) then
message(_M, "Could not render genome graph", 0x00990000)
end
end
if _M.currentFrame%5 == 0 then
evaluateCurrent(_M)
end
for b=0,#config.ButtonNames - 1,1 do
if controller[b] then
input.set(0, b, 1)
else
input.set(0, b, 0)
end
end
game.getPositions()
local timeoutConst = 0
if game.vertical then
timeoutConst = config.NeatConfig.TimeoutConstant * 10
else
timeoutConst = config.NeatConfig.TimeoutConstant
end
-- Don't punish being launched by barrels
-- FIXME Will this skew mine cart levels?
if game.getVelocityY() < -2104 then
message(_M, "BARREL! "..frame, 0x00ffff00)
if _M.timeout < timeoutConst + 60 * 12 then
_M.timeout = _M.timeout + 60 * 12
end
end
local nextArea = game.getCurrentArea()
if nextArea ~= _M.lastArea then
_M.lastArea = nextArea
game.onceAreaLoaded(function()
message(_M, "Loady")
_M.timeout = _M.timeout + 60 * 5
_M.currentArea = nextArea
_M.lastArea = _M.currentArea
if _M.rightmost[_M.currentArea] == nil then
_M.rightmost[_M.currentArea] = 0
_M.upmost[_M.currentArea] = 0
end
end)
end
if not game.vertical then
if game.partyX > _M.rightmost[_M.currentArea] then
_M.rightmost[_M.currentArea] = game.partyX
if _M.timeout < timeoutConst then
_M.timeout = timeoutConst
end
end
else
if game.partyY > _M.upmost[_M.currentArea] then
_M.upmost[_M.currentArea] = game.partyY
if _M.timeout < timeoutConst then
_M.timeout = timeoutConst
end
end
end
-- FIXME Measure distance to target / area exit
-- We might not always be horizontal
local hitTimer = game.getHitTimer(_M.lastBoth)
if hitTimer > 0 then
_M.partyHitCounter = _M.partyHitCounter + 1
--message(_M, "party took damage, hit counter: " .. _M.partyHitCounter)
end
local powerUp = game.getBoth()
_M.lastBoth = powerUp
if powerUp > 0 then
if powerUp ~= _M.powerUpBefore then
_M.powerUpCounter = _M.powerUpCounter + 1
_M.powerUpBefore = powerUp
end
end
local krem = game.getKremCoins() - _M.startKrem
if krem > _M.lastKrem then
message(_M, string.format("Kremcoin grabbed: %d", _M.timeout), 0x00009900)
_M.lastKrem = krem
_M.timeout = _M.timeout + 60 * 10
end
_M.timeout = _M.timeout - 1
-- Continue if we haven't timed out
local timeoutBonus = _M.currentFrame / 4
if _M.timeout + timeoutBonus > 0 then
mainLoop(_M, genome)
return
end
-- Timeout calculations beyond this point
-- Manipulating the timeout value won't have
-- any effect
local bananas = game.getBananas() - _M.startBananas
local coins = game.getCoins() - _M.startCoins
local kong = game.getKong()
message(_M, string.format("Bananas: %d, coins: %d, Krem: %d, KONG: %d", bananas, coins, krem, kong))
local bananaCoinsFitness = (krem * 100) + (kong * 60) + (bananas * 50) + (coins * 0.2)
if (bananas + coins) > 0 then
message(_M, "Bananas, Coins, KONG added " .. bananaCoinsFitness .. " fitness")
end
local hitPenalty = _M.partyHitCounter * 100
local bumpPenalty = _M.bumps * 100
local powerUpBonus = _M.powerUpCounter * 100
local most = 0
if not game.vertical then
for k,v in pairs(_M.rightmost) do
most = most + v
end
most = most - _M.currentFrame / 2
else
for k,v in pairs(_M.upmost) do
most = most + v
end
most = most - _M.currentFrame / 2
end
local fitness = bananaCoinsFitness - bumpPenalty - hitPenalty + powerUpBonus + most + game.getJumpHeight() / 100
local lives = game.getLives()
if _M.startLives < lives then
local extraLiveBonus = (lives - _M.startLives)*1000
fitness = fitness + extraLiveBonus
message(_M, "Extra live bonus added " .. extraLiveBonus)
end
if game.getGoalHit() then
fitness = fitness + 1000
message(_M, string.format("LEVEL WON! Fitness: %d", fitness), 0x0000ff00)
end
if fitness == 0 then
fitness = -1
end
genome.fitness = fitness
if fitness > _M.maxFitness then
_M.maxFitness = fitness
end
if _M.genomeCallback ~= nil then
_M.genomeCallback(genome, _M.currentGenomeIndex)
end
message(_M, string.format("Gen %d species %d genome %d fitness: %d", _M.currentGenerationIndex, _M.currentSpecies.id, _M.currentGenomeIndex, fitness))
_M.currentGenomeIndex = 1
while fitnessAlreadyMeasured(_M) do
_M.currentGenomeIndex = _M.currentGenomeIndex + 1
if _M.currentGenomeIndex > #_M.currentSpecies.genomes then
for i=#_M.dereg,1,-1 do
local d = table.remove(_M.dereg, i)
callback.unregister(d[1], d[2])
end
input.keyhook("1", false)
input.keyhook("4", false)
input.keyhook("6", false)
input.keyhook("8", false)
input.keyhook("9", false)
input.keyhook("tab", false)
_M.finishCallback()
return
end
end
initializeRun(_M, function()
mainLoop(_M, genome)
end)
end)
end
local function newNeuron()
local neuron = {}
neuron.incoming = {}
@ -630,45 +461,290 @@ local function generateNetwork(genome)
genome.network = network
end
local function elapsed(_M)
if config.StartPowerup ~= NIL then
game.writePowerup(config.StartPowerup)
end
_M.currentFrame = 0
_M.timeout = config.NeatConfig.TimeoutConstant
-- Kill the run if we go back to the map screen
game.onceMapLoaded(function()
_M.timeout = -100000
local beginRewindState = nil
local function rewind()
return game.rewind(beginRewindState):next(function()
frame = 0
lastFrame = 0
end)
_M.bumps = 0
-- Penalize player for collisions that do not result in enemy deaths
game.onEmptyHit(function()
_M.bumps = _M.bumps + 1
end)
game.clearJoypad()
_M.startKong = game.getKong()
_M.startBananas = game.getBananas()
_M.startKrem = game.getKremCoins()
_M.lastKrem = _M.startKrem
_M.startCoins = game.getCoins()
_M.startLives = game.getLives()
_M.partyHitCounter = 0
_M.powerUpCounter = 0
_M.powerUpBefore = game.getBoth()
_M.currentArea = game.getCurrentArea()
_M.lastArea = _M.currentArea
_M.rightmost = { [_M.currentArea] = 0 }
_M.upmost = { [_M.currentArea] = 0 }
local genome = _M.currentSpecies.genomes[_M.currentGenomeIndex]
generateNetwork(genome)
evaluateCurrent(_M)
for i=#_M.runInitialized,1,-1 do
table.remove(_M.runInitialized, i)()
end
end
local function rewound()
set_timer_timeout(1)
local function initializeRun(_M)
settings.set_speed("turbo")
-- XXX Does this actually work or only affects new VM loads?
settings.set('lua-maxmem', 1024)
local enableSound = 'on'
if config.NeatConfig.DisableSound then
enableSound = 'off'
end
exec('enable-sound '..enableSound)
gui.subframe_update(false)
return rewind():next(function()
bsnes.enablelayer(0, 0, true)
bsnes.enablelayer(0, 1, false)
bsnes.enablelayer(1, 0, false)
bsnes.enablelayer(1, 1, false)
bsnes.enablelayer(2, 0, false)
bsnes.enablelayer(2, 1, false)
bsnes.enablelayer(3, 0, false)
bsnes.enablelayer(3, 1, false)
bsnes.enablelayer(4, 0, true)
bsnes.enablelayer(4, 1, true)
bsnes.enablelayer(4, 2, true)
bsnes.enablelayer(4, 3, true)
if config.StartPowerup ~= nil then
game.writePowerup(config.StartPowerup)
end
_M.currentFrame = 0
_M.timeout = config.NeatConfig.TimeoutConstant
-- Kill the run if we go back to the map screen
game.onceMapLoaded(function()
_M.timeout = -100000
end)
_M.bumps = 0
-- Penalize player for collisions that do not result in enemy deaths
game.onEmptyHit(function()
_M.bumps = _M.bumps + 1
end)
game.clearJoypad()
_M.startKong = game.getKong()
_M.totalBananas = 0
_M.lastBananas = game.getBananas()
_M.startKrem = game.getKremCoins()
_M.lastKrem = _M.startKrem
_M.startCoins = game.getCoins()
_M.startLives = game.getLives()
_M.partyHitCounter = 0
_M.powerUpCounter = 0
_M.powerUpBefore = game.getBoth()
_M.currentArea = game.getCurrentArea()
_M.lastArea = _M.currentArea
for _,areaInfo in pairs(_M.areaInfo) do
for i=1,#areaInfo.waypoints,1 do
local waypoint = areaInfo.waypoints[i]
waypoint.shortest = waypoint.startDistance
end
end
local genome = _M.currentSpecies.genomes[_M.currentGenomeIndex]
generateNetwork(genome)
local inputs, inputDeltas = game.getInputs()
evaluateCurrent(_M, inputs, inputDeltas)
end)
end
local function mainLoop(_M, genome)
return game.advanceFrame():next(function()
local nextArea = game.getCurrentArea()
if nextArea ~= _M.lastArea then
_M.lastArea = nextArea
game.onceAreaLoaded(function()
message(_M, 'Loaded area '..nextArea)
_M.timeout = _M.timeout + 60 * 5
_M.currentArea = nextArea
_M.lastArea = _M.currentArea
end)
elseif _M.currentArea == _M.lastArea and _M.areaInfo[_M.currentArea] == nil then
message(_M, 'Searching for the main exit in this area')
return game.findPreferredExit():next(function(preferredExit)
local areaInfo = {
preferredExit = preferredExit,
waypoints = game.getWaypoints(preferredExit.x, preferredExit.y),
}
table.insert(areaInfo.waypoints, 1, preferredExit)
for i=#areaInfo.waypoints,1,-1 do
local waypoint = areaInfo.waypoints[i]
if waypoint.y > game.partyY + mem.size.tile * 7 then
message(_M, string.format('Skipped waypoint %d,%d', waypoint.x, waypoint.y), 0x00ffff00)
table.remove(areaInfo.waypoints, i)
goto continue
end
local startDistance = math.floor(math.sqrt((waypoint.y - game.partyY) ^ 2 + (waypoint.x - game.partyX) ^ 2))
waypoint.startDistance = startDistance
waypoint.shortest = startDistance
::continue::
end
message(_M, string.format('Found %d waypoints', #areaInfo.waypoints))
_M.areaInfo[_M.currentArea] = areaInfo
end)
end
end):next(function()
if lastFrame + 1 ~= frame then
message(_M, string.format("We missed %d frames", frame - lastFrame), 0x00ff0000)
end
lastFrame = frame
if genome ~= nil then
_M.currentFrame = _M.currentFrame + 1
end
genome = _M.currentSpecies.genomes[_M.currentGenomeIndex]
if _M.drawFrame % 10 == 0 then
displayGenome(genome)
end
game.getPositions()
local timeoutConst = config.NeatConfig.TimeoutConstant
local fell = game.fell()
if (fell or game.diedFromHit()) and _M.timeout > 0 then
_M.timeout = 0
end
if _M.currentFrame % 5 == 0 then
local sprites = game.getSprites()
local inputs, inputDeltas = game.getInputs(sprites)
if game.bonusScreenDisplayed(inputs) and _M.timeout > -1000 and _M.timeout < timeoutConst then
_M.timeout = timeoutConst
end
evaluateCurrent(_M, inputs, inputDeltas)
end
local areaInfo = _M.areaInfo[_M.currentArea]
if areaInfo ~= nil and game.partyY ~= 0 and game.partyX ~= 0 then
for i=1,#areaInfo.waypoints,1 do
local waypoint = areaInfo.waypoints[i]
local dist = math.floor(math.sqrt((waypoint.y - game.partyY) ^ 2 + (waypoint.x - game.partyX) ^ 2))
if dist < waypoint.shortest then
waypoint.shortest = dist
if _M.timeout < timeoutConst then
_M.timeout = timeoutConst
end
end
end
end
local hitTimer = game.getHitTimer(_M.lastBoth)
if hitTimer > 0 then
_M.partyHitCounter = _M.partyHitCounter + 1
--message(_M, "party took damage, hit counter: " .. _M.partyHitCounter)
end
local powerUp = game.getBoth()
_M.lastBoth = powerUp
if powerUp > 0 then
if powerUp ~= _M.powerUpBefore then
_M.powerUpCounter = _M.powerUpCounter + 1
_M.powerUpBefore = powerUp
end
end
local krem = game.getKremCoins() - _M.startKrem
if krem > _M.lastKrem then
message(_M, string.format("Kremcoin grabbed: %d", _M.timeout), 0x00009900)
_M.lastKrem = krem
_M.timeout = _M.timeout + 60 * 10
end
local currentBananas = game.getBananas()
local moreBananas = currentBananas - _M.lastBananas
if moreBananas > 0 then
_M.totalBananas = _M.totalBananas + moreBananas
end
_M.lastBananas = currentBananas
_M.timeout = _M.timeout - 1
if lastFrame ~= frame then
message(_M, string.format("We missed %d frames", frame - lastFrame), 0x00990000)
end
-- Continue if we haven't timed out
local timeoutBonus = _M.currentFrame / 4
if _M.timeout + timeoutBonus > 0 then
return mainLoop(_M, genome)
end
-- Timeout calculations beyond this point
-- Manipulating the timeout value won't have
-- any effect
local coins = game.getCoins() - _M.startCoins
local kong = game.getKong()
message(_M, string.format("Bananas: %d, coins: %d, Krem: %d, KONG: %d", _M.totalBananas, coins, krem, kong))
local bananaCoinsFitness = (krem * 100) + (kong * 60) + (_M.totalBananas * 50) + (coins * 0.2)
if (_M.totalBananas + coins) > 0 then
message(_M, "Bananas, Coins, KONG added " .. bananaCoinsFitness .. " fitness")
end
local hitPenalty = _M.partyHitCounter * 100
local bumpPenalty = _M.bumps * 100
local powerUpBonus = _M.powerUpCounter * 100
local distanceTraversed = getDistanceTraversed(_M.areaInfo) - _M.currentFrame / 2
local fitness = bananaCoinsFitness - bumpPenalty - hitPenalty + powerUpBonus + distanceTraversed
if fell then
fitness = fitness / 10
message(_M, "Fall penalty 1/10")
end
local lives = game.getLives()
if _M.startLives < lives then
local extraLiveBonus = (lives - _M.startLives)*1000
fitness = fitness + extraLiveBonus
message(_M, "Extra live bonus added " .. extraLiveBonus)
end
local sprites = game.getSprites()
-- FIXME We should test this before we time out
if game.getGoalHit(sprites) then
fitness = fitness + 1000
message(_M, string.format("LEVEL WON! Fitness: %d", fitness), 0x0000ff00)
end
if fitness == 0 then
fitness = -1
end
genome.fitness = fitness
if _M.maxFitness == nil or fitness > _M.maxFitness then
_M.maxFitness = fitness
end
if _M.genomeCallback ~= nil then
_M.genomeCallback(genome, _M.currentGenomeIndex)
end
message(_M, string.format("Gen %d species %d genome %d fitness: %d", _M.currentGenerationIndex, _M.currentSpecies.id, _M.currentGenomeIndex, math.floor(fitness)))
_M.currentGenomeIndex = 1
while fitnessAlreadyMeasured(_M) do
_M.currentGenomeIndex = _M.currentGenomeIndex + 1
if _M.currentGenomeIndex > #_M.currentSpecies.genomes then
game.unregisterHandlers()
for i=#_M.dereg,1,-1 do
local d = table.remove(_M.dereg, i)
callback.unregister(d[1], d[2])
end
input.keyhook("1", false)
input.keyhook("4", false)
input.keyhook("6", false)
input.keyhook("8", false)
input.keyhook("9", false)
input.keyhook("tab", false)
return _M.maxFitness
end
end
return initializeRun(_M):next(function()
return mainLoop(_M, genome)
end)
end)
end
local function register(_M, name, func)
@ -700,17 +776,30 @@ local function onLoad(_M, handler)
table.insert(_M.onLoadHandler, handler)
end
local function reset(_M)
for i=#_M.onResetHandler,1,-1 do
_M.onResetHandler[i]()
end
message(_M, "Will be reset once all currently active threads finish", 0x00990000)
end
local function onReset(_M, handler)
table.insert(_M.onResetHandler, handler)
end
local function keyhook (_M, key, state)
if state.value == 1 then
if key == "tab" then
_M.inputmode = not _M.inputmode
_M.helddown = key
elseif inputmode then
elseif _M.inputmode then
return
elseif key == "1" then
_M.helddown = key
config.Running = not config.Running
elseif key == "4" then
-- FIXME Should be handled similarly to other events
_M.helddown = key
pool.requestTop()
elseif key == "6" then
@ -721,21 +810,21 @@ local function keyhook (_M, key, state)
load(_M)
elseif key == "9" then
_M.helddown = key
pool.run(true)
reset(_M)
end
elseif state.value == 0 then
helddown = nil
_M.helddown = nil
end
end
local function saveLoadInput(_M)
local inputs = input.raw()
if not inputmode then
if not _M.inputmode then
-- FIXME
_M.saveLoadFile = config.NeatConfig.SaveFile
return
end
if helddown == nil then
if _M.helddown == nil then
local mapping = {
backslash = "\\",
colon = ":",
@ -773,7 +862,7 @@ local function saveLoadInput(_M)
end
if k == "back" then
config.NeatConfig.SaveFile = config.NeatConfig.SaveFile:sub(1, #config.NeatConfig.SaveFile-1)
helddown = k
_M.helddown = k
goto continue
end
local m = k
@ -784,38 +873,35 @@ local function saveLoadInput(_M)
goto continue
end
config.NeatConfig.SaveFile = config.NeatConfig.SaveFile..m
helddown = k
_M.helddown = k
::continue::
end
elseif helddown ~= nil and inputs[helddown]["value"] ~= 1 then
helddown = nil
elseif _M.helddown ~= nil and inputs[_M.helddown]["value"] ~= 1 then
_M.helddown = nil
end
end
local function run(_M, species, generationIdx, genomeCallback, finishCallback)
local function run(_M, species, generationIdx, genomeCallback)
if beginRewindState == nil then
beginRewindState = movie.to_rewind(config.NeatConfig.Filename)
end
game.registerHandlers()
_M.currentGenerationIndex = generationIdx
_M.currentSpecies = species
_M.currentGenomeIndex = 1
_M.genomeCallback = genomeCallback
_M.finishCallback = finishCallback
register(_M, 'paint', function()
painting(_M)
end)
register(_M, 'input', function()
processFrameAdvanced(_M)
end)
register(_M, 'input', function()
frame = frame + 1
updateController()
saveLoadInput(_M)
end)
register(_M, 'keyhook', function(key, state)
keyhook(_M, key, state)
end)
register(_M, 'post_rewind', rewound)
register(_M, 'timer', function()
elapsed(_M)
end)
input.keyhook("1", true)
input.keyhook("4", true)
@ -824,8 +910,8 @@ local function run(_M, species, generationIdx, genomeCallback, finishCallback)
input.keyhook("9", true)
input.keyhook("tab", true)
initializeRun(_M, function()
mainLoop(_M)
return initializeRun(_M):next(function()
return mainLoop(_M)
end)
end
@ -837,15 +923,19 @@ local function onRenderForm(_M, handler)
table.insert(_M.onRenderFormHandler, handler)
end
return function()
return function(promise)
Promise = promise
if game == nil then
game = dofile(base.."/game.lua")(Promise)
end
local _M = {
currentGenerationIndex = 1,
currentSpecies = nil,
finishCallback = nil,
genomeCallback = nil,
currentGenomeIndex = 1,
currentFrame = 0,
maxFitness = 0,
drawFrame = 0,
maxFitness = nil,
dereg = {},
inputmode = false,
@ -855,7 +945,8 @@ return function()
timeout = 0,
bumps = 0,
startKong = 0,
startBananas = 0,
lastBananas = 0,
totalBananas = 0,
startKrem = 0,
lastKrem = 0,
startCoins = 0,
@ -865,18 +956,14 @@ return function()
powerUpBefore = 0,
currentArea = 0,
lastArea = 0,
rightmost = {},
upmost = {},
areaInfo = {},
lastBoth = 0,
runInitialized = {},
onMessageHandler = {},
onSaveHandler = {},
onLoadHandler = {},
onResetHandler = {},
onRenderFormHandler = {},
onFrameAdvancedHandler = {},
}
_M.onRenderForm = function(handler)
@ -895,9 +982,13 @@ return function()
onLoad(_M, handler)
end
_M.run = function(species, generationIdx, genomeCallback, finishCallback)
run(_M, species, generationIdx, genomeCallback, finishCallback)
_M.onReset = function(handler)
onReset(_M, handler)
end
_M.run = function(species, generationIdx, genomeCallback)
return run(_M, species, generationIdx, genomeCallback)
end
return _M
end
end

View file

@ -4,100 +4,115 @@ _M.Sprites = {}
-- Make sure this list is sorted before initialization.
_M.NeutralSprites = {
0x0020, -- Krow egg fragments
0x0060, -- Barrel fragments
0x0064, -- Barrel fragments
krowEggFragments = 0x0020,
barrelFragments = 0x0060,
barrelFragments2 = 0x0064,
-- Our heroes
0x00e4, -- Diddy
0x00e8, -- Dixie
0x0100, -- Stars
diddy = 0x00e4,
dixie = 0x00e8,
stars = 0x0100,
-- Items that require too much interaction
0x01a4, -- Barrel
0x01b0, -- Cannonball (immobile)
0x01c0, -- Chest
0x01bc, -- Small crate
0x011c, -- Barrel
0x013c, -- Cannon
0x014c, -- Hook
0x01b8, -- TNT
barrel = 0x01a4, -- Barrel
cannonball = 0x01b0,
chest = 0x01c0,
smallCrate = 0x01bc,
barrel2 = 0x011c,
cannon = 0x013c,
hook = 0x014c,
tnt = 0x01b8,
-- Inert
0x0168, -- Goal pole
0x016c, -- Goal roulette
0x0160, -- Goal base
0x0164, -- Goal barrel
goalPole = 0x0168,
goalroulette = 0x016c,
goalBase = 0x0160,
goalBarrel = 0x0164,
0x0238, -- Pow
0x023c, -- Exploding crate
0x0258, -- No Animals Sign
pow = 0x0238,
explodingCrate = 0x023c,
noAnimalsSign = 0x0258,
}
-- Make sure this list is sorted before initialization.
_M.GoodSprites = {
-- Destinations
0x0094, -- Area exit
0x00b0, -- Goal target
areaExit = 0x0094,
goalTarget = 0x00b0,
0x0120, -- Bonus barrel
0x0128, -- Hot air balloon
0x0140, -- Launch barrel
0x0148, -- Animal crate
0x0150, -- Invincibility barrel
0x0154, -- Midpoint
0x015c, -- Banana Coin/Kremkoin/DK Coin
0x0170, -- Banana bunch
0x0174, -- KONG letters
0x0178, -- xUP balloon
bonusBarrel = 0x0120,
hotAirBalloon = 0x0128,
launchBarrel = 0x0140,
animalCrate = 0x0148,
invincibilityBarrel = 0x0150,
midpoint = 0x0154,
allCoins = 0x015c, -- Banana Coin/Kremkoin/DK Coin
bananaBunch = 0x0170,
kongLetter = 0x0174,
upBalloon = 0x0178, -- xUP balloon
-- Animals
0x0190, -- Squitter
0x0194, -- Rattly
0x0198, -- Squawks
0x019c, -- Rambi
0x0304, -- Clapper
squitter = 0x0190,
rattly = 0x0194,
squawks = 0x0198,
rambi = 0x019c,
clapper = 0x0304,
0x01a8, -- DK Barrel label
dkBarrelLabel = 0x01a8,
0x01b4, -- Krow's eggs
krowEgg = 0x01b4,
0x0220, -- Flitter (used as unavoidable platforms in some levels)
0x02d4, -- Krochead (red and green)
flitter = 0x0220,
krocheadAllColors = 0x02d4,
}
-- Currently not used.
_M.BadSprites = {
-- Baddies
0x006c, -- Kannon
0x01ac, -- Klobber (yellow and green)
0x01d0, -- Kannon's fodder (Ball/barrel)
0x01d8, -- Krusha
0x01dc, -- Click-Clack
0x01e4, -- Neek
0x01ec, -- Klomp
0x01e8, -- Klobber (awake)
0x01f0, -- Klampon
0x01f8, -- Flotsam
0x0200, -- Klinger
0x0208, -- Puftup
0x0218, -- Zinger (red and yellow)
0x0214, -- Mini-Necky
0x020c, -- Lockjaw
0x021c, -- Kaboing
0x0224, -- Krow (Boss)
0x025c, -- Krook (very large)
kannon = 0x006c,
klobberAllColors = 0x01ac,
kannonFodder = 0x01d0,
krusha = 0x01d8,
clickClack = 0x01dc,
neek = 0x01e4,
klomp = 0x01ec,
klobberAwake = 0x01e8,
klampon = 0x01f0,
flotsam = 0x01f8,
klinger = 0x0200,
klingerSkidCloud = 0x0014,
puftup = 0x0208,
zingerAllColors = 0x0218,
miniNecky = 0x0214,
lockjaw = 0x020c,
kaboing = 0x021c,
krow = 0x0224, -- Boss
krook = 0x025c,
}
_M.SpriteNames = {}
function _M.InitSpriteNames()
for v,k in pairs(_M.GoodSprites) do
_M.SpriteNames[k] = v
end
for v,k in pairs(_M.BadSprites) do
_M.SpriteNames[k] = v
end
for v,k in pairs(_M.NeutralSprites) do
_M.SpriteNames[k] = v
end
end
function _M.InitSpriteList()
for i=1,#_M.GoodSprites,1 do
_M.Sprites[_M.GoodSprites[i]] = 1
for k,v in pairs(_M.GoodSprites) do
_M.Sprites[v] = 1
end
for i=1,#_M.BadSprites,1 do
_M.Sprites[_M.BadSprites[i]] = -1
for k,v in pairs(_M.BadSprites) do
_M.Sprites[v] = -1
end
for i=1,#_M.NeutralSprites,1 do
_M.Sprites[_M.NeutralSprites[i]] = 0
for k,v in pairs(_M.NeutralSprites) do
_M.Sprites[v] = 0
end
end
@ -108,21 +123,21 @@ _M.ExtNeutralSprites = {
}
_M.ExtGoodSprites = {
0xe0, -- banana
0xe1, -- banana
0xe2, -- banana
0xe3, -- banana
0xe4, -- banana
0xe5, -- banana
0xe6, -- banana
0xe7, -- banana
0xe8, -- banana
0xe9, -- banana
0xea, -- banana
0xeb, -- banana
0xec, -- banana
0xed, -- banana
0xee, -- banana
banana01 = 0xe0, -- banana
banana02 = 0xe1, -- banana
banana03 = 0xe2, -- banana
banana04 = 0xe3, -- banana
banana05 = 0xe4, -- banana
banana06 = 0xe5, -- banana
banana07 = 0xe6, -- banana
banana08 = 0xe7, -- banana
banana09 = 0xe8, -- banana
banana10 = 0xe9, -- banana
banana11 = 0xea, -- banana
banana12 = 0xeb, -- banana
banana13 = 0xec, -- banana
banana14 = 0xed, -- banana
banana15 = 0xee, -- banana
}
-- Currently not used.
@ -130,14 +145,14 @@ _M.ExtBadSprites = {
}
function _M.InitExtSpriteList()
for i=1,#_M.ExtGoodSprites,1 do
_M.extSprites[_M.ExtGoodSprites[i]] = 1
for k,v in pairs(_M.ExtGoodSprites) do
_M.extSprites[v] = 1
end
for i=1,#_M.ExtBadSprites,1 do
_M.extSprites[_M.ExtBadSprites[i]] = -1
for k,v in pairs(_M.ExtBadSprites) do
_M.extSprites[v] = -1
end
for i=1,#_M.ExtNeutralSprites,1 do
_M.extSprites[_M.ExtNeutralSprites[i]] = 0
for k,v in pairs(_M.ExtNeutralSprites) do
_M.extSprites[v] = 0
end
end

View file

@ -1,20 +1,19 @@
PARTY_X = 0x7e0a2a
TILE_SIZE = 32
local memory, movie, utime, callback, set_timer_timeout, input, gui, exec, settings = memory, movie, utime, callback, set_timer_timeout, input, gui, exec, settings
print(memory.readword(PARTY_X))
local base = string.gsub(@@LUA_SCRIPT_FILENAME@@, "(.*[/\\])(.*)", "%1")
local Promise = dofile(base.."/promise.lua")
callback.register('timer', function()
Promise.update()
set_timer_timeout(1)
end)
set_timer_timeout(1)
local game = dofile(base.."/game.lua")(Promise)
local util = dofile(base.."/util.lua")(Promise)
local serpent = dofile(base.."/serpent.lua")
function on_post_rewind()
print("Async?")
print(memory.readword(PARTY_X))
end
game.registerHandlers()
game.getPositions()
function movement(addr, val)
if memory.readword(addr) > TILE_SIZE * 20 then
local rew = movie.to_rewind("pool/PiratePanic.lsmv")
movie.unsafe_rewind(rew)
print("Sync?")
print(memory.readword(PARTY_X))
end
end
memory2.WRAM:registerwrite(0x0a2a, movement)
game.findPreferredExit():next(function(preferredExit)
game.getWaypoints(preferredExit.x, preferredExit.y)
end)

151
tools/bsnes-launcher.lua Normal file
View file

@ -0,0 +1,151 @@
local base = string.gsub(@@LUA_SCRIPT_FILENAME@@, "(.*[/\\])(.*)", "%1").."/.."
local util = dofile(base.."/util.lua")()
local config = dofile(base.."/config.lua")
local mem = dofile(base.."/mem.lua")
local function text(text)
io.stderr:write(text)
io.stderr:write('\n')
print(text)
end
local function help()
text([[
Syntax: BSNES_LAUNCHER_ARGS='<arguments>' lsnes --lua=]]..base..[[/bsnes-launcher.lua
--sprite-startindex=<number> Which sprite to start at. Can be a value from 0-22
Sprite breakpoint arguments:
These will create breakpoints for all sprite slots with properties matching the
given pattern.
]])
for propName,_ in pairs(mem.offset.sprite) do
text('--sprite-'..propName..'<Breakpoint format>')
end
text([[
Breakpoint format: --<switchname>[=<value>][:<rwx>]
rwx = read / write / execute flags
For example, --sprite-x:r would match any reads of any sprite X position
--sprite-x>10:w would match any writes of any sprite X position greater
than 0x10 (16). Omitting the rwx will create the breakpoints with the values
specified but they will not trigger until you enable them manually.
Example:
Matching values of the goalpost sprites in Pirate Panic:
LSNES_HIDE_MESSAGES=1 \
LSNES_HIDE_STATUSPANEL=1 \
BSNES_LAUNCHER_ARGS='--sprite-startindex=2 --sprite-control=160:w --sprite-control=164:w --sprite-control=16c:w --sprite-control=168:w --sprite-x=1c3c:w --sprite-x=1c48:w --sprite-x=1c5d:w --sprite-y=ff63:w --sprite-y=ff8f:w' \
lsnes --lua=tools/bsnes-launcher.lua
Note that ff is included in some values since bsnes-plus can only match one
byte at a time, so sometimes has false positives on those bytes since the true
value is not unique enough.
]])
end
local bps = {}
--- Add breakpoint switch
---@param switchName string The name of the switch, without dashes
---@param source string address space
---@param startAddress integer The start address
---@param arg string The argument to test
---@param valWidth integer byte size of value - defaults to 2
---@param addrWidth integer byte size of address - defaults to 3
local function bpSwitch(switchName, source, startAddress, arg, valWidth, addrWidth)
if addrWidth == nil then
addrWidth = 3
end
if valWidth == nil then
valWidth = 2
end
if arg:sub(1, #switchName+2) == '--'..switchName then
local op, valHex, rwx = arg:sub(#switchName+3):match('^([><=]*)([0-9a-fA-F]*):?([rwxRWX]*)$')
local valPad = ''
if valHex ~= '' then
local val = tonumber(valHex, 16)
valPad = string.format('%0'..(valWidth*2)..'x', val)
end
local fmt = '%0'..(addrWidth*2)..'x%s%s:%s:%s'
if valWidth == 2 then
-- SNES is LE!
local valLeast = valPad:sub(3, 4)
local valMost = valPad:sub(1, 2)
local opLeast = ''
local opMost = ''
-- FIXME This is surely wrong but better than nothing?
if op == '>' then
opLeast = '>'
opMost = '>='
elseif op == '=' then
opLeast = '='
opMost = '='
elseif op == '<' then
opLeast = '<'
opMost = '<='
elseif op == '<=' then
opLeast = '<='
opMost = '<='
elseif op == '>=' then
opLeast = '>='
opMost = '>='
else
valLeast = ''
valMost = ''
end
table.insert(bps, string.format(fmt, startAddress, opLeast, valLeast, rwx, source))
table.insert(bps, string.format(fmt, startAddress + 1, opMost, valMost, rwx, source))
elseif valWidth == 1 then
table.insert(bps, string.format(fmt, startAddress, op, valPad, rwx, source))
end
end
end
local count = 0
local startIndex = 0
for arg in os.getenv('BSNES_LAUNCHER_ARGS'):gmatch('[^ ]+') do
if arg:sub(1,20) == '--sprite-startindex=' then
startIndex = tonumber(arg:sub(21))
end
count = count + 1
for propName,offset in pairs(mem.offset.sprite) do
for i=startIndex,22,1 do
local startAddress = mem.addr.spriteBase + mem.size.sprite * i + offset
bpSwitch('sprite-'..propName, 'cpu', startAddress, arg)
end
end
end
local cmdFmt = 'bsnes %s--show-debugger "%s"'
local bpArgs = ''
local withoutBps = string.format(cmdFmt, bpArgs, config.ROM)
if #bps > 0 then
bpArgs = '-b "'..table.concat(bps, '" -b "')..'" '
end
local withBps = string.format(cmdFmt, bpArgs, config.ROM)
if count == 0 then
text('====================')
help()
end
text('====================')
text('Note that you will need to turn off breakpoint saving for this app to work correctly.')
text('')
text(withBps)
text('====================')
while true do
text('Starting without breakpoints first. Save your state where you want with F2, then quit to load with breakpoints enabled')
util.doCmd(withoutBps)
text('Starting with breakpoints enabled. Load your state with F4')
util.doCmd(withBps)
end

View file

@ -1,52 +1,53 @@
local base = string.gsub(@@LUA_SCRIPT_FILENAME@@, "(.*[/\\])(.*)", "%1")
local base = string.gsub(@@LUA_SCRIPT_FILENAME@@, "(.*[/\\])(.*)", "%1").."/.."
local util = dofile(base.."/util.lua")
local set_timer_timeout, memory, memory2, gui, input, bit, callback = set_timer_timeout, memory, memory2, gui, input, bit, callback
local warn = '========== The ROM must be running before running this script'
io.stderr:write(warn)
print(warn)
local Promise = dofile(base.."/promise.lua")
callback.register('timer', function()
Promise.update()
set_timer_timeout(1)
end)
set_timer_timeout(1)
local util = dofile(base.."/util.lua")(Promise)
local mem = dofile(base.."/mem.lua")
local spritelist = dofile(base.."/spritelist.lua")
local game = dofile(base.."/game.lua")
local config = dofile(base.."/config.lua")
local game = dofile(base.."/game.lua")(Promise)
spritelist.InitSpriteList()
spritelist.InitExtSpriteList()
FG_COLOR = 0x00ffffff
BG_COLOR = 0x99000000
ENEMY_SIZE = 64
TILEDATA_POINTER = 0x7e0098
TILE_SIZE = 32
TILE_RADIUS = 5
SPRITE_BASE = 0x7e0de2
SOLID_LESS_THAN = 0x7e00a0
DIDDY_X_VELOCITY = 0x7e0e02
DIDDY_Y_VELOCITY = 0x7e0e06
DIXIE_X_VELOCITY = 0x7e0e60
DIXIE_Y_VELOCITY = 0x7e0e64
STAGE_NUMBER = 0x7e08a8
STAGE_NUMBER_MOVEMENT = 0x7e08c8
CAMERA_X = 0x7e17ba
CAMERA_Y = 0x7e17c0
CAMERA_MODE = 0x7e054f
TILE_COLLISION_MATH_POINTER = 0x7e17b2
VERTICAL_POINTER = 0xc414
PARTY_X = 0x7e0a2a
PARTY_Y = 0x7e0a2c
game.registerHandlers()
count = 0
detailsidx = -1
jumping = false
helddown = false
floatmode = false
rulers = true
pokemon = false
pokecount = 0
showhelp = false
locked = false
lockdata = nil
incsprite = 0
questionable_tiles = false
local CAMERA_MODE = 0x7e054f
local DIDDY_X_VELOCITY = 0x7e0e02
local DIDDY_Y_VELOCITY = 0x7e0e06
local DIXIE_X_VELOCITY = 0x7e0e60
local DIXIE_Y_VELOCITY = 0x7e0e64
local FG_COLOR = 0x00ffffff
local BG_COLOR = 0x99000000
local TILE_RADIUS = 5
font = gui.font.load(base.."font.font")
local frame = 0
local detailsidx = -1
local jumping = false
local helddown = false
local floatmode = false
local rulers = true
local pokemon = false
local pokecount = 0
local showhelp = false
local locked = false
local lockdata = nil
local incsprite = 0
local questionable_tiles = false
function text(x, y, msg, fg, bg)
local font = gui.font.load(base.."/font.font")
local function text(x, y, msg, fg, bg)
if fg == nil then
fg = FG_COLOR
end
@ -136,48 +137,48 @@ function on_input (subframe)
end
end
function get_sprite(base_addr)
local cameraX = memory.readword(CAMERA_X) - 256
local cameraY = memory.readword(CAMERA_Y) - 256
local x = memory.readword(base_addr + 0x06)
local y = memory.readword(base_addr + 0x0a)
local function get_sprite(baseAddr)
local spriteData = memory.readregion(baseAddr, mem.size.sprite)
local offsets = mem.offset.sprite
local x = util.regionToWord(spriteData, offsets.x)
local y = util.regionToWord(spriteData, offsets.y)
return {
base_addr = string.format("%04x", base_addr),
screenX = x - 256 - cameraX,
screenY = y - 256 - cameraY - TILE_SIZE / 3,
control = memory.readword(base_addr),
draworder = memory.readword(base_addr + 0x02),
base_addr = string.format("%04x", baseAddr),
screenX = x - game.cameraX,
screenY = y - game.cameraY - mem.size.tile / 3,
control = util.regionToWord(spriteData, offsets.control),
draworder = util.regionToWord(spriteData, 0x02),
x = x,
y = y,
jumpheight = memory.readword(base_addr + 0x0e),
style = memory.readword(base_addr + 0x12),
currentframe = memory.readword(base_addr + 0x18),
nextframe = memory.readword(base_addr + 0x1a),
state = memory.readword(base_addr + 0x1e),
velox = memory.readsword(base_addr + 0x20),
veloy = memory.readsword(base_addr + 0x24),
velomaxx = memory.readsword(base_addr + 0x26),
velomaxy = memory.readsword(base_addr + 0x2a),
motion = memory.readword(base_addr + 0x2e),
attr = memory.readword(base_addr + 0x30),
animnum = memory.readword(base_addr + 0x36),
remainingframe = memory.readword(base_addr + 0x38),
animcontrol = memory.readword(base_addr + 0x3a),
animreadpos = memory.readword(base_addr + 0x3c),
animcontrol2 = memory.readword(base_addr + 0x3e),
animformat = memory.readword(base_addr + 0x40),
damage1 = memory.readword(base_addr + 0x44),
damage2 = memory.readword(base_addr + 0x46),
damage3 = memory.readword(base_addr + 0x48),
damage4 = memory.readword(base_addr + 0x4a),
damage5 = memory.readword(base_addr + 0x4c),
damage6 = memory.readword(base_addr + 0x4e),
spriteparam = memory.readword(base_addr + 0x58),
jumpHeight = util.regionToWord(spriteData, offsets.jumpHeight),
style = util.regionToWord(spriteData, offsets.style),
currentframe = util.regionToWord(spriteData, 0x18),
nextframe = util.regionToWord(spriteData, 0x1a),
state = util.regionToWord(spriteData, 0x1e),
velocityX = util.regionToSWord(spriteData, offsets.velocityX),
velocityY = util.regionToSWord(spriteData, offsets.velocityY),
velomaxx = util.regionToSWord(spriteData, 0x26),
velomaxy = util.regionToSWord(spriteData, 0x2a),
motion = util.regionToWord(spriteData, offsets.motion),
attr = util.regionToWord(spriteData, 0x30),
animnum = util.regionToWord(spriteData, 0x36),
remainingframe = util.regionToWord(spriteData, 0x38),
animcontrol = util.regionToWord(spriteData, 0x3a),
animreadpos = util.regionToWord(spriteData, 0x3c),
animcontrol2 = util.regionToWord(spriteData, 0x3e),
animformat = util.regionToWord(spriteData, 0x40),
damage1 = util.regionToWord(spriteData, 0x44),
damage2 = util.regionToWord(spriteData, 0x46),
damage3 = util.regionToWord(spriteData, 0x48),
damage4 = util.regionToWord(spriteData, 0x4a),
damage5 = util.regionToWord(spriteData, 0x4c),
damage6 = util.regionToWord(spriteData, 0x4e),
spriteparam = util.regionToWord(spriteData, 0x58),
}
end
function sprite_details(idx)
local base_addr = idx * 94 + SPRITE_BASE
local function sprite_details(idx)
local base_addr = idx * mem.size.sprite + mem.addr.spriteBase
local sprite = get_sprite(base_addr)
@ -197,20 +198,22 @@ function sprite_details(idx)
end
if locked and lockdata == nil then
lockdata = memory.readregion(base_addr, 94)
lockdata = memory.readregion(base_addr, mem.size.sprite)
end
if lockdata ~= nil and locked then
memory.writeregion(base_addr, 94, lockdata)
memory.writeregion(base_addr, mem.size.sprite, lockdata)
end
text(0, 0, "Sprite "..idx..(locked and " (Locked)" or "")..":\n\n"..util.table_to_string(sprite))
end
function on_paint (not_synth)
count = count + 1
local guiWidth, guiHeight = gui.resolution()
local waypoints = {}
local overlayCtx = nil
local overlay = nil
local function renderOverlay(guiWidth, guiHeight)
overlayCtx:set()
overlayCtx:clear()
if showhelp then
text(0, 0, [[
@ -230,9 +233,13 @@ Sprite Details:
[8] Enable / Disable stage tile rulers
[9] Enable / Disable hidden tiles
]])
overlay = overlayCtx:render()
gui.renderctx.setnull()
return
end
game.getPositions()
local toggles = ""
if pokemon then
@ -256,34 +263,28 @@ Sprite Details:
"Up"
}
local cameraX = memory.readword(CAMERA_X) - 256
local cameraY = memory.readword(CAMERA_Y) - 256
local cameraDir = memory.readbyte(CAMERA_MODE)
local direction = directions[cameraDir+1]
local vertical = memory.readword(TILE_COLLISION_MATH_POINTER) == VERTICAL_POINTER
local partyX = memory.readword(PARTY_X)
local partyY = memory.readword(PARTY_Y)
local partyTileOffset = game.tileOffsetCalculation(partyX, partyY, vertical)
local partyTileOffset = game.tileOffsetCalculation(game.partyX, game.partyY, game.vertical)
local stats = string.format([[
%s camera %d,%d
Vertical: %s
Tile offset: %04x
Stage number: %04x
Stage (movement): %04x
Main area: %04x
Current area: %04x
%s
]], direction, cameraX, cameraY, vertical, partyTileOffset, memory.readword(STAGE_NUMBER), memory.readword(STAGE_NUMBER_MOVEMENT), util.table_to_string(game.getInputs()):gsub("[\\{\\},\n\"]", ""):gsub("-1", "X"):gsub("0", "."):gsub("1", "O"):gsub("(.............)", "%1\n"))
]], direction, game.cameraX, game.cameraY, game.vertical, partyTileOffset, memory.readword(mem.addr.mainAreaNumber), memory.readword(mem.addr.currentAreaNumber), util.table_to_string(game.getInputs()):gsub("[\\{\\},\n\"]", ""):gsub("-1", "X"):gsub("0", "."):gsub("1", "O"):gsub("(.............)", "%1\n"))
text(guiWidth - 125, guiHeight - 200, stats)
text((partyX - 256 - cameraX) * 2, (partyY - 256 - cameraY) * 2 + 20, "Party")
text((game.partyX - game.cameraX) * 2, (game.partyY - game.cameraY) * 2 + 20, "Party")
local sprites = {}
for idx = 0,22,1 do
local base_addr = idx * 94 + SPRITE_BASE
local base_addr = idx * mem.size.sprite + mem.addr.spriteBase
local sprite = get_sprite(base_addr)
@ -305,38 +306,63 @@ Stage (movement): %04x
text(sprite.screenX * 2, sprite.screenY * 2, string.format("%04x, %04x, %04x", sprite.control, sprite.animnum, sprite.attr), FG_COLOR, sprcolor)
local filename = os.getenv("HOME").."/neat-donk/catchem/"..sprite.animnum..","..sprite.attr..".png"
if pokemon and spriteScreenX > (guiWidth / 4) and spriteScreenX < (guiWidth / 4) * 3 and spriteScreenY > (guiHeight / 3) and spriteScreenY < guiHeight and not util.file_exists(filename) then
if pokemon and sprite.screenX > (guiWidth / 4) and sprite.screenY < (guiWidth / 4) * 3 and sprite.screenY > (guiHeight / 3) and sprite.screenY < guiHeight and not util.file_exists(filename) then
gui.screenshot(filename)
pokecount = pokecount + 1
end
::continue::
end
if rulers and cameraX >= 0 then
for i=1,#waypoints,1 do
local screenX = (waypoints[i].x - game.cameraX) * 2
local screenY = (waypoints[i].y - game.cameraY) * 2
if screenX > guiWidth - mem.size.tile * 2 then
screenX = guiWidth - mem.size.tile * 2
end
if screenY > guiHeight then
screenY = guiHeight - 20
end
if screenX < 0 then
screenX = 0
end
if screenY < 0 then
screenY = 0
end
text(screenX, screenY, "WAYPOINT "..i)
end
text(guiWidth / 2, guiHeight - 20, "WAYPOINTS: "..#waypoints)
if rulers and game.cameraX >= 0 then
local halfWidth = math.floor(guiWidth / 2)
local halfHeight = math.floor(guiHeight / 2)
local cameraTileX = math.floor(cameraX / TILE_SIZE)
local cameraTileX = math.floor(game.cameraX / mem.size.tile)
gui.line(0, halfHeight, guiWidth, halfHeight, BG_COLOR)
for i = cameraTileX, cameraTileX + guiWidth / TILE_SIZE / 2,1 do
text((i * TILE_SIZE - cameraX) * 2, halfHeight, tostring(i), FG_COLOR, BG_COLOR)
for i = cameraTileX, cameraTileX + guiWidth / mem.size.tile / 2,1 do
text((i * mem.size.tile - game.cameraX) * 2, halfHeight, tostring(i), FG_COLOR, BG_COLOR)
end
local cameraTileY = math.floor(cameraY / TILE_SIZE)
local cameraTileY = math.floor(game.cameraY / mem.size.tile)
gui.line(halfWidth, 0, halfWidth, guiHeight, BG_COLOR)
for i = cameraTileY, cameraTileY + guiHeight / TILE_SIZE / 2,1 do
text(halfWidth, (i * TILE_SIZE - cameraY) * 2, tostring(i), FG_COLOR, BG_COLOR)
for i = cameraTileY, cameraTileY + guiHeight / mem.size.tile / 2,1 do
text(halfWidth, (i * mem.size.tile - game.cameraY) * 2, tostring(i), FG_COLOR, BG_COLOR)
end
end
local tilePtr = memory.readhword(TILEDATA_POINTER)
local tilePtr = memory.readhword(mem.addr.tiledataPointer)
for x = -TILE_RADIUS, TILE_RADIUS, 1 do
for y = -TILE_RADIUS, TILE_RADIUS, 1 do
local tileX = math.floor((partyX + x * TILE_SIZE) / TILE_SIZE) * TILE_SIZE
local tileY = math.floor((partyY + y * TILE_SIZE) / TILE_SIZE) * TILE_SIZE
local tileX = math.floor((game.partyX + x * mem.size.tile) / mem.size.tile) * mem.size.tile
local tileY = math.floor((game.partyY + y * mem.size.tile) / mem.size.tile) * mem.size.tile
local offset = game.tileOffsetCalculation(tileX, tileY, vertical)
local offset = game.tileOffsetCalculation(tileX, tileY, game.vertical)
local tile = memory.readword(tilePtr + offset)
@ -344,8 +370,8 @@ Stage (movement): %04x
goto continue
end
local screenX = (tileX - 256 - cameraX) * 2
local screenY = (tileY - 256 - cameraY) * 2
local screenX = (tileX - 256 - game.cameraX) * 2
local screenY = (tileY - 256 - game.cameraY) * 2
if screenX < 0 or screenX > guiWidth or
screenY < 0 or screenY > guiHeight then
--goto continue
@ -357,7 +383,7 @@ Stage (movement): %04x
end
end
if cameraX >= 0 then
if game.cameraX >= 0 then
local oam = memory2.OAM:readregion(0x00, 0x220)
for idx=0,0x200/4-1,1 do
@ -369,7 +395,7 @@ Stage (movement): %04x
flags = oam[idx * 4 + 4],
}
if screenSprite.x < 0 or screenSprite.y > guiHeight / 2 or screenSprite.y < TILE_SIZE then
if screenSprite.x < 0 or screenSprite.y > guiHeight / 2 or screenSprite.y < mem.size.tile then
goto continue
end
@ -378,8 +404,8 @@ Stage (movement): %04x
if sprite.control == 0 then
goto nextsprite
end
if screenSprite.x > sprite.screenX - ENEMY_SIZE and screenSprite.x < sprite.screenX + ENEMY_SIZE / 2 and
screenSprite.y > sprite.screenY - ENEMY_SIZE and screenSprite.y < sprite.screenY then
if screenSprite.x > sprite.screenX - mem.size.enemy and screenSprite.x < sprite.screenX + mem.size.enemy / 2 and
screenSprite.y > sprite.screenY - mem.size.enemy and screenSprite.y < sprite.screenY then
goto continue
end
::nextsprite::
@ -400,10 +426,26 @@ Stage (movement): %04x
end
text(guiWidth - 125, 20, "Help [Hold 0]")
overlay = overlayCtx:render()
gui.renderctx.setnull()
end
function on_timer()
set_timer_timeout(100 * 1000)
function on_paint (not_synth)
frame = frame + 1
local guiWidth, guiHeight = gui.resolution()
if overlayCtx == nil then
overlayCtx = gui.renderctx.new(guiWidth, guiHeight)
end
if frame % 3 == 0 then
renderOverlay(guiWidth, guiHeight)
end
if overlay ~= nil then
overlay:draw(0, 0)
end
end
input.keyhook("1", true)
@ -417,4 +459,16 @@ input.keyhook("8", true)
input.keyhook("9", true)
input.keyhook("0", true)
set_timer_timeout(100 * 1000)
game.findPreferredExit():next(function(preferredExit)
return game.getWaypoints(preferredExit.x, preferredExit.y)
end):next(function(w)
waypoints = w
end)
-- fe0a58 crate: near bunch and klomp on barrels
-- fe0a58: Crate X position
-- fe0a60: Crate Y position
-- fe0a70 bunch: near crate and klomp on barrels
-- fe0a70: X position
-- fe0a72: Y position

311
util.lua
View file

@ -1,5 +1,268 @@
local utime, bit, callback, exec = utime, bit, callback, exec
local base = string.gsub(@@LUA_SCRIPT_FILENAME@@, "(.*[/\\])(.*)", "%1")
local Promise = nil
local _M = {}
_M.isWin = package.config:sub(1, 1) == '\\'
--- Converts a function into a promise.
--- Useful for decoupling code from the original event it was fired in.
---@param next function The function to resolve on the next tick
---@return Promise Promise A promise that returns the value of the next function
function _M.promiseWrap(next, value)
local promise = Promise.new()
promise:resolve(value)
return promise:next(next)
end
local times = {}
--- Track how long a function takes
---@param toCall function Function that executes synchronously
---@param name string What to print in the finish text
---@return any any The original value
function _M.time(toCall, name)
if name == nil then
name = 'function'
end
local sec, usec = utime()
local startTime = sec * 1000000 + usec
local finishTime = 0
local ret = toCall()
sec, usec = utime()
finishTime = sec * 1000000 + usec
local t = times[name]
if t == nil then
t = {}
times[name] = t
end
if #t > 50 then
table.remove(t, 1)
end
t[#t+1] = finishTime - startTime
local sum = 0
for i=1,#t,1 do
sum = sum + t[i]
end
print(name..' is averaging '..math.floor(sum / #t))
return ret
end
--- Wait for a specified amount of time. Note that this is dependent on the
--- timer timeout getting set elsewhere in the code, probably in the Promise
--- handler setup
---@param delayUsec number Number of microseconds to wait
---@return Promise Promise A promise that resolves when the time has elapsed
function _M.delay(delayUsec)
return Promise.new(function(res, rej)
local sec, usec = utime()
local start = sec * 1000000 + usec
local finish = start
local unTimer = nil
local function onTimer()
sec, usec = utime()
finish = sec * 1000000 + usec
if finish - start >= delayUsec then
callback.unregister('timer', unTimer)
res()
end
end
unTimer = callback.register('timer', onTimer)
end)
end
function _M.saveState(filename)
return Promise.new(function(res, rej)
local unSave = nil
local unErrSave = nil
local function errSave(f)
if f == filename then
callback.unregister('post_save', unSave)
callback.unregister('err_save', unErrSave)
rej()
end
end
local function postSave(f)
if f == filename then
callback.unregister('post_save', unSave)
callback.unregister('err_save', unErrSave)
res()
end
end
unSave = callback.register('post_save', postSave)
unErrSave = callback.register('err_save', errSave)
exec('save-state '..filename)
end)
end
function _M.loadAndStart(romFile)
return Promise.new(function(res, rej)
local unPaint = nil
local paint = 0
local function onPaint()
paint = paint + 1
_M.promiseWrap(function()
if paint == 1 then
exec('pause-emulator')
elseif paint > 1 then
callback.unregister('paint', unPaint)
res()
end
end)
end
unPaint = callback.register('paint', onPaint)
exec('load-rom '..romFile)
end)
end
function _M.getTempDir()
local temps = {
os.getenv("TMPDIR"),
os.getenv("TEMP"),
os.getenv("TEMPDIR"),
os.getenv("TMP"),
}
local tempDir = "/tmp"
for i=1,#temps,1 do
local temp = temps[i]
if temp ~= nil and temp ~= "" then
tempDir = temps[i]
break
end
end
return tempDir
end
--- Echo a command, run it, and return the file handle
--- @param cmd string The command to execute
--- @param workdir string The working directory
--- @param env table The environment variables
function _M.popenCmd(cmd, workdir, env)
local cmdParts = {}
if workdir ~= nil then
if _M.isWin then
table.insert(cmdParts, 'cd /d "'..workdir..'" &&')
else
table.insert(cmdParts, 'cd "'..workdir..'" &&')
end
end
if env ~= nil then
for k,v in pairs(env) do
if _M.isWin then
table.insert(cmdParts, string.format("set %s=%s&&", k, v))
else
table.insert(cmdParts, string.format("%s='%s'", k, v))
end
end
end
table.insert(cmdParts, cmd)
local fullCmd = table.concat(cmdParts, " ")
print(fullCmd)
--[[ local dummy = "/dev/null"
if isWin then
dummy = "NUL"
end
return io.open(dummy, 'r') ]]
return io.popen(fullCmd, 'r')
end
--- Echo a command, run it, and handle any errors
--- @return string string The stdout
function _M.doCmd(...)
return _M.scrapeCmd('*a', ...)
end
function _M.openReadPipe(name)
if _M.isWin then
local cmd = 'cd /d "'..base..'" && "'..base..'/namedpipe/createAndReadPipe.exe" "'..name..'"'
print(cmd)
return io.popen(cmd, 'r')
else
return io.popen("socat 'UNIX-LISTEN:".._M.getTempDir().."/"..name.."' -", 'r')
end
end
function _M.openReadPipeWriter(name)
local writer = nil
while writer == nil do
if _M.isWin then
writer = io.open('\\\\.\\pipe\\'..name, 'w')
else
writer = io.popen("socat 'UNIX-CONNECT:".._M.getTempDir().."/"..name.."' -", 'w')
end
end
return writer
end
--- Download a url
--- @param url string URI of resource to download
--- @param dest string File to save resource to
function _M.downloadFile (url, dest)
return _M.doCmd('curl -sL "'..url..'" > "'..dest..'" || wget -qO- "'..url..'" > "'..dest..'"')
end
--- Unzip a ZIP file with unzip or tar
--- @param zipfile string The ZIP file path
--- @param dest string Where to unzip the ZIP file. Beware ZIP bombs.
function _M.unzip (zipfile, dest)
return _M.doCmd('tar -xvf "'..zipfile..'" 2>&1 || unzip -n "'..zipfile..'" -d "'..dest..
'" 2>&1', dest)
end
--- Create a directory
--- @return string dir The directory to create
function _M.mkdir(dir)
if _M.isWin then
return _M.doCmd('if not exist "'..dir..'" mkdir "'..dir..'"')
else
return _M.doCmd("mkdir '"..dir.."'")
end
end
--- Run a command and get the output
--- @param formats table|string|number List or single io.read() specifier
--- @return table table List of results based on read specifiers
function _M.scrapeCmd(formats, ...)
local poppet = _M.popenCmd(...)
local outputs = nil
if type(formats) ~= 'table' then
outputs = poppet:read(formats)
else
outputs = {}
for i=1,#formats,1 do
table.insert(outputs, poppet:read(formats[i]))
end
end
_M.closeCmd(poppet)
return outputs
end
--- Check the command's exit code and throw a Lua error if it isn't right
--- @param handle file* The handle of the command
function _M.closeCmd(handle)
local ok, state, code = handle:close()
if state ~= "exit" then
return
end
if code ~= 0 then
error(string.format("The last command failed: %s %d", state, code))
end
end
function _M.table_to_string(tbl)
local result = "{"
local keys = {}
@ -43,4 +306,50 @@ function _M.file_exists(name)
if f~=nil then io.close(f) return true else return false end
end
return _M
function _M.nearestColor(needle, colors)
local opacity = bit.band(needle, 0xff000000)
local needle = {
r = bit.lrshift(bit.band(needle, 0x00ff0000), 4),
g = bit.lrshift(bit.band(needle, 0x0000ff00), 2),
b = bit.band(needle, 0x000000ff),
}
local minDistanceSq = 0x7fffffff
local value = nil
for name,color in pairs(colors) do
local distanceSq = (
math.pow(needle.r - color.r, 2) +
math.pow(needle.g - color.g, 2) +
math.pow(needle.b - color.b, 2)
)
if distanceSq < minDistanceSq then
minDistanceSq = distanceSq
value = name
end
if value == nil then
value = name
end
end
return value
end
function _M.regionToWord(region, offset)
return bit.compose(region[offset], region[offset + 1])
end
function _M.regionToSWord(region, offset)
local unsigned = _M.regionToWord(region, offset)
local base = bit.band(unsigned, 0x7fff)
local sign = bit.lrshift(unsigned, 15)
-- There is a more mathematical way to do this but whatever
if sign == 1 then
return base-0x8000
else
return base
end
end
return function(promise)
Promise = promise
return _M
end

View file

@ -3,10 +3,12 @@ SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )"
#ARGS=("--lua=$SCRIPT_DIR/neat-donk.lua")
ARGS=()
PORT=5309
(
while ! nc -z localhost $PORT ; do
sleep 0.1
done
xdg-open http://127.0.0.1:$PORT
) &
xpra start --bind-tcp=127.0.0.1:$PORT --html=on \
--start-child="lsnes ${ARGS[*]}" \
--exit-with-child=yes --start-new-commands=no
while ! nc -z localhost $PORT ; do
sleep 0.1
done
xdg-open http://127.0.0.1:$PORT
--start-child="/etc/alternatives/x-terminal-emulator -e 'lsnes ${ARGS[*]}'" \
--exit-with-child=yes --start-new-commands=no --daemon=no

12
xpra.css Normal file
View file

@ -0,0 +1,12 @@
#screen .wmclass-Lsnes, #screen .window[class*="term"] {
display: block;
position: relative;
top: 0 !important;
left: 0 !important;
}
#screen {
display: flex;
flex-wrap: wrap;
justify-content: space-around;
}

193
zzlib.lua Normal file
View file

@ -0,0 +1,193 @@
-- zzlib - zlib decompression in Lua - Implementation-independent code
-- Copyright (c) 2016-2020 Francois Galea <fgalea at free.fr>
-- This program is free software. It comes without any warranty, to
-- the extent permitted by applicable law. You can redistribute it
-- and/or modify it under the terms of the Do What The Fuck You Want
-- To Public License, Version 2, as published by Sam Hocevar. See
-- the COPYING file or http://www.wtfpl.net/ for more details.
local base = string.gsub(@@LUA_SCRIPT_FILENAME@@, "(.*[/\\])(.*)", "%1")
local unpack = table.unpack or unpack
local infl = dofile(base.."/inflate-bit32.lua")
local zzlib = {}
local function arraytostr(array)
local tmp = {}
local size = #array
local pos = 1
local imax = 1
while size > 0 do
local bsize = size>=2048 and 2048 or size
local s = string.char(unpack(array,pos,pos+bsize-1))
pos = pos + bsize
size = size - bsize
local i = 1
while tmp[i] do
s = tmp[i]..s
tmp[i] = nil
i = i + 1
end
if i > imax then
imax = i
end
tmp[i] = s
end
local str = ""
for i=1,imax do
if tmp[i] then
str = tmp[i]..str
end
end
return str
end
local function inflate_gzip(bs)
local id1,id2,cm,flg = bs.buf:byte(1,4)
if id1 ~= 31 or id2 ~= 139 then
error("invalid gzip header")
end
if cm ~= 8 then
error("only deflate format is supported")
end
bs.pos=11
if infl.band(flg,4) ~= 0 then
local xl1,xl2 = bs.buf.byte(bs.pos,bs.pos+1)
local xlen = xl2*256+xl1
bs.pos = bs.pos+xlen+2
end
if infl.band(flg,8) ~= 0 then
local pos = bs.buf:find("\0",bs.pos)
bs.pos = pos+1
end
if infl.band(flg,16) ~= 0 then
local pos = bs.buf:find("\0",bs.pos)
bs.pos = pos+1
end
if infl.band(flg,2) ~= 0 then
-- TODO: check header CRC16
bs.pos = bs.pos+2
end
local result = arraytostr(infl.main(bs))
local crc = bs:getb(8)+256*(bs:getb(8)+256*(bs:getb(8)+256*bs:getb(8)))
bs:close()
if crc ~= infl.crc32(result) then
error("checksum verification failed")
end
return result
end
-- compute Adler-32 checksum
local function adler32(s)
local s1 = 1
local s2 = 0
for i=1,#s do
local c = s:byte(i)
s1 = (s1+c)%65521
s2 = (s2+s1)%65521
end
return s2*65536+s1
end
local function inflate_zlib(bs)
local cmf = bs.buf:byte(1)
local flg = bs.buf:byte(2)
if (cmf*256+flg)%31 ~= 0 then
error("zlib header check bits are incorrect")
end
if infl.band(cmf,15) ~= 8 then
error("only deflate format is supported")
end
if infl.rshift(cmf,4) ~= 7 then
error("unsupported window size")
end
if infl.band(flg,32) ~= 0 then
error("preset dictionary not implemented")
end
bs.pos=3
local result = arraytostr(infl.main(bs))
local adler = ((bs:getb(8)*256+bs:getb(8))*256+bs:getb(8))*256+bs:getb(8)
bs:close()
if adler ~= adler32(result) then
error("checksum verification failed")
end
return result
end
function zzlib.gunzipf(filename)
local file,err = io.open(filename,"rb")
if not file then
return nil,err
end
return inflate_gzip(infl.bitstream_init(file))
end
function zzlib.gunzip(str)
return inflate_gzip(infl.bitstream_init(str))
end
function zzlib.inflate(str)
return inflate_zlib(infl.bitstream_init(str))
end
local function int2le(str,pos)
local a,b = str:byte(pos,pos+1)
return b*256+a
end
local function int4le(str,pos)
local a,b,c,d = str:byte(pos,pos+3)
return ((d*256+c)*256+b)*256+a
end
function zzlib.unzip(buf,filename)
local p = #buf-21
local quit = false
if int4le(buf,p) ~= 0x06054b50 then
-- not sure there is a reliable way to locate the end of central directory record
-- if it has a variable sized comment field
error(".ZIP file comments not supported")
end
local cdoffset = int4le(buf,p+16)
local nfiles = int2le(buf,p+10)
p = cdoffset+1
for i=1,nfiles do
if int4le(buf,p) ~= 0x02014b50 then
error("invalid central directory header signature")
end
local flag = int2le(buf,p+8)
local method = int2le(buf,p+10)
local crc = int4le(buf,p+16)
local namelen = int2le(buf,p+28)
local name = buf:sub(p+46,p+45+namelen)
if name == filename then
local headoffset = int4le(buf,p+42)
p = 1+headoffset
if int4le(buf,p) ~= 0x04034b50 then
error("invalid local header signature")
end
local csize = int4le(buf,p+18)
local extlen = int2le(buf,p+28)
p = p+30+namelen+extlen
if method == 0 then
-- no compression
result = buf:sub(p,p+csize-1)
else
-- DEFLATE compression
local bs = infl.bitstream_init(buf)
bs.pos = p
result = arraytostr(infl.main(bs))
end
if crc ~= infl.crc32(result) then
error("checksum verification failed")
end
return result
end
p = p+46+namelen+int2le(buf,p+30)+int2le(buf,p+32)
end
error("file '"..filename.."' not found in ZIP archive")
end
return zzlib