to put all of the shapes into there "corresponding holes" as I've called them, I needed to use a script, last time I think I gave a pretty vague description of how this script works.
So this time ill explain it and try to give actual code lines to really make a good understanding for any publishers seeing this
SO what exactly should this script do?
It needs to:
- Enter a seed (RAM ADRESS 0x20)
- Read through Mainmemory all the positions of the shapes and their holes
- Calculate how long it would take (How many movements and rotations need to be done) to start at the beginning of a level, put all the shapes in and then get to the end
- Print in the LUA Console output the result of the seed
Now how did I do this? first we need to learn how we can "Enter a seed". The value of 0x20 on the frame the RESET button is pressed is the seed that you are inputing, so all I had to do was edit 0x20 with code and we can start at 0 and go up until 255:
local Rng_Byte=0x20
local currentseedcheck=0
while currentseedcheck<=255 do
mainmemory.writebyte(Rng_Byte,currentseedcheck)
emu.frameadvance();
print(currentseedcheck)--------------------says the seed before telling me the least amount of movements for better formatting
print(findbestrouteforseed())---------------gives lowest amount of moves for this seed
tastudio.setplayback(4596)-----------------In this case frame 4596 is the frame where RESET was pressed
currentseedcheck=currentseedcheck+1
end
this code is very simple and it gives a very easy solution to this problem
now onto step 2 which is ALOT MORE COMPLEX. The way my code finds the best route for the seed goes as so:
I have a function which can find the fastest route from 1 square to anotherill explain how this function works later
using this function the script starts at the players starting location for the level to a shape, then that shapes hole then the next shape
too find the best order of which shape to go to I do this:
local mapprioritypoints={{square,squarehole},{circle,circlehole},{M,Mhole},{X,Xhole}}
local allorders={{1,2,3,4},{1,2,4,3},{1,3,2,4},{1,3,4,2},{1,4,2,3},{1,4,3,2},
{2,1,3,4},{2,1,4,3},{2,3,1,4},{2,3,4,1},{2,4,1,3},{2,4,3,1},
{3,1,2,4},{3,1,4,2},{3,2,1,4},{3,2,4,1},{3,4,1,2},{3,4,2,1},
{4,1,2,3},{4,1,3,2},{4,2,1,3},{4,2,3,1},{4,3,1,2},{4,3,2,1}}
local function findbestrouteforseed()
local best={["M"]=999,["R"]=999} ------------------------"M" means movements and "R" rotations
for _, order in ipairs(allorders) do-------------------------goes through every order
for _,number in ipairs(order) do--------------goes through every number and the numbers represent different shapes
for i=1,2 do --------------------goes to the shape and then its hole
to={mainmemory.readbyte(mapprioritypoints[number][i])}
addingfunction()
end
end
end
end
this code bruteforses gets the movements for every order of shape collection. And with this code I can very easily pick the best one. here's the full code for the findbestrouteforseed()
function just incase you don't understand how the rest is done:
local function findbestrouteforseed()
local best={["M"]=999,["R"]=999}
for _, order in ipairs(allorders) do
local adding={["M"]=0,["R"]=0}
local plrposition={7,"Down"}-----------------------the beginning square that needs to be manually changed based on the level
local to={0}
local function addingfunction(exact)-------------------this exact parameter determines if it should go directly to the block or to just face its direction, this is needed beacuse to get a shape you just need to be facing it not go to its exact location and I figured to make it an option for the end block as well
local bestroutetoshape=pathfind(plrposition,to,exact)
adding["M"]=adding["M"]+bestroutetoshape[1]["Moves"]
adding["R"]=adding["R"]+bestroutetoshape[1]["Rots"]
plrposition=bestroutetoshape[2]
end
for _,number in ipairs(order) do
for i=1,2 do
to={mainmemory.readbyte(mapprioritypoints[number][i])}
addingfunction()
end
end
to={9,"Down"}------------exit that needs to be manually changed based on the level
addingfunction(true)
if adding["M"]<best["M"] then
best=adding
else
if adding["R"]<best["R"] and adding["M"]==best["M"] then
best=adding
end
end
end
return best
end
so now that thats explained I need to talk about how pathfind()
works which gets the shortest amount of moves from 1 point to another
so the function obviously needs to have the parameters of a block to start at then a block to end at so:
local pathfind(start,end,exact)-exact parameter determines if it should go directly to the block or to just face its direction, this is needed because to get a shape you just need to be facing it not go to its exact location
now how does this function actually do the task ahead of it?
- It starts at its start block which was set in the parameters and moves forward until it finds a wall ahead of it.
- For every block it moves it looks to the square at the left and right of it to see if theirs a possible turn it could make, If there is its added to its paths table.
- after the end of the process of moving forward until it cant, it looks through the paths table and starts the process over but from those points
also for every step it eliminates the block behind it so the pathfinding system cant go back on its old path and make a infinite loop
lets show how i did this in code!
so first i used a table to define where all the walls are for the level and it looks like this:
-----1=free square
-----0=allsides blocked
-----2=left
-----3=right
-----4=up
-----5=down
-----6=up and down
-----7=left and right
----8=everything but up
----9=everything but down
----10=everything but left
----11=everything but right
----12=up and left
----13=up and right
----14=down and left
----15=down and right
local map={
1 ,1 ,1 ,1 ,1 ,1 ,1 ,1 ,
1 ,8 ,9 ,9 ,9 ,1 ,0 ,1 ,
1 ,1 ,8 ,8 ,8 ,1 ,1 ,1 ,
1 ,9 ,9 ,9 ,9 ,1 ,13 ,1 ,
1 ,8 ,8 ,8 ,8 ,4 ,15 ,1 ,
1 ,1 ,9 ,9 ,9 ,1 ,1 ,1 ,
1 ,0 ,8 ,8 ,8 ,1 ,0 ,1 ,
1 ,1 ,1 ,1 ,3 ,1 ,1 ,1 ,
}
this was my map table for level 5. It can look pretty confusing at first glance but I put comments so that it can be easier.
1 means there are no walls around that square and as it says 10 would mean that there are walls in every direction except down and etc..
I also had to make a map for the doors that can be entered through 1 side but once you go through you cant go back. Basically the same concept though:
--------0=none
--------1=left
--------2=right
--------3=up
--------4=down
local one_way_walls_map={
0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,
0,2,2,2,4,0,0,0,
2,2,2,2,0,0,0,0,
0,0,0,0,2,0,0,0,
0,2,2,2,3,0,0,0,
0,0,0,0,0,0,0,0,
0,0,0,0,0,1,0,0,
}
--------------------------this code is also necessary because if all the indexes were integers you cant refence something like one_way_walls_map[2] and get 0 it would give back 2 because it thinks your talking about (find index with value 2) and not (get index 2) so i make it strings to prevent that. also it turns index 0 to index 1 and 1->2 and so on because thats how the game reads the grid map as well.
for i,v in ipairs(map) do
if v==0 then map[i-1]="#" end
if v==1 then map[i-1]=" " end
if v==2 then map[i-1]="a" end
if v==3 then map[i-1]="b" end
if v==4 then map[i-1]="c" end
if v==5 then map[i-1]="d" end
if v==6 then map[i-1]="e" end
if v==7 then map[i-1]="f" end
if v==8 then map[i-1]="g" end
if v==9 then map[i-1]="h" end
if v==10 then map[i-1]="i" end
if v==11 then map[i-1]="j" end
if v==12 then map[i-1]="k" end
if v==13 then map[i-1]="l" end
if v==14 then map[i-1]="m" end
if v==15 then map[i-1]="n" end
end
for i,v in ipairs(one_way_walls_map) do
if v==0 then one_way_walls_map[i-1]=" " end
if v==1 then one_way_walls_map[i-1]="a" end
if v==2 then one_way_walls_map[i-1]="b" end
if v==3 then one_way_walls_map[i-1]="c" end
if v==4 then one_way_walls_map[i-1]="d" end
end
one_way_walls_map[64]=nil
map[64]=nil
so now that we have a representation of what the map is we need to define some other things to make this possible too
heres a list of those things:
----------------------------------------defines the added number for every direction
local directions={
["Up"]=-8,
["Down"]=8,
["Left"]=-1,
["Right"]=1,
}
-------------------------------------------------makes walls work
local walls={
["#"]={["Left"]=true,["Right"]=true,["Up"]=true,["Down"]=true},
[" "]={["Left"]=false,["Right"]=false,["Up"]=false,["Down"]=false},
["a"]={["Left"]=true,["Right"]=false,["Up"]=false,["Down"]=false},
["b"]={["Left"]=false,["Right"]=true,["Up"]=false,["Down"]=false},
["c"]={["Left"]=false,["Right"]=false,["Up"]=true,["Down"]=false},
["d"]={["Left"]=false,["Right"]=false,["Up"]=false,["Down"]=true},
["e"]={["Left"]=false,["Right"]=false,["Up"]=true,["Down"]=true},
["f"]={["Left"]=true,["Right"]=true,["Up"]=false,["Down"]=false},
["g"]={["Left"]=true,["Right"]=true,["Up"]=false,["Down"]=true},
["h"]={["Left"]=true,["Right"]=true,["Up"]=true,["Down"]=false},
["i"]={["Left"]=false,["Right"]=true,["Up"]=true,["Down"]=true},
["j"]={["Left"]=true,["Right"]=false,["Up"]=true,["Down"]=true},
["k"]={["Left"]=true,["Right"]=false,["Up"]=true,["Down"]=false},
["l"]={["Left"]=false,["Right"]=true,["Up"]=true,["Down"]=false},
["m"]={["Left"]=true,["Right"]=false,["Up"]=false,["Down"]=true},
["n"]={["Left"]=false,["Right"]=true,["Up"]=false,["Down"]=true},
}
----------------------------left and right for every direction
local oposeingdirections={
["Up"]={"Left","Right"},
["Down"]={"Left","Right"},
["Left"]={"Up","Down"},
["Right"]={"Up","Down"},
}
--------------------------opposite of every direction
local oposeingdirections2={
["Up"]="Down",
["Down"]="Up",
["Left"]="Right",
["Right"]="Left",
}
now I use a GoStright()
function inside of the pathfind()
function to make the part I said before. It needs to go straight until it finds a wall then look make at all the turns it could have made and do the same thing
now the GoStright()
function has these parameters:
local function GoStright(startpos,eliminated_blocks,moves,Rotations,lookback)
startpos is self explanatory
eliminated_blocks is a table of all the blocks it already went through as I've said before to eliminate the inability of an infinite loop
moves and rotations is the amount of moves and rotations the player has executed from its starting point to where it is now
lookback is a bool. if its set to true it looks behind the player on the first block that was set for the gostraight function and if that block doesn't have a wall it puts it into paths table. its only the first block because every other time the check would find a wall due to the eliminated_blocks always putting a new wall behind after every move
now to significantly speed up the process of explaining the rest of the GoStright code ill just paste it here with comments that explains what everything does
local paths={}
local plrpos=startpos[1]-------------------------startpos is actually put in as a table to get the pos and rotation
local plrRot=startpos[2]
local one_way_walls={
[" "]=nil,
["a"]="Left",
["b"]="Right",
["c"]="Up",
["d"]="Down",
}
local function testforwall(testrot)-------------------------this is the test for wall funtion
---------------------------------this prevents any overlaping
if testrot=="Left" then
for i=0,7 do
if plrpos==(8*i) then
return true
end
end
end
if testrot=="Right" then
for i=1,8 do
if plrpos==(8*i)-1 then
return true
end
end
end
---------------------------------------------------------
-----------------------------------------one way wall collision
if one_way_walls[one_way_walls_map[plrpos]]==testrot then
return false
end
--------------------------------------------------
--------------------------------------------------tests if there is a wall in the direction specified
if ((not map[plrpos]) or walls[map[plrpos]][testrot]) or ((not map[plrpos+directions[testrot]]) or walls[map[plrpos+directions[testrot]]][oposeingdirections2[testrot]]) then
return true
end
----------------------------------------
end
local function lookforwinningblock()
-----------------------------------unorganized goofy ahh code that looks in all directions for the block that was put as targetblock[2](targetblock[1] would be the desired rotation it would end up on)
if exact then
if targetblock[1]==plrpos then
if targetblock[2] then
if targetblock[2]==plrRot then
return {["paths"]=paths,["moves"]=moves,["Rotations"]=Rotations,["plrpos"]=plrpos,["plrRot"]=targetblock[2], ["Found_end"] = true}
end
if targetblock[2]==oposeingdirections2[plrRot] then
return {["paths"]=paths,["moves"]=moves,["Rotations"]=Rotations+2,["plrpos"]=plrpos,["plrRot"]=targetblock[2], ["Found_end"] = true}
end
return {["paths"]=paths,["moves"]=moves,["Rotations"]=Rotations+1,["plrpos"]=plrpos,["plrRot"]=targetblock[2], ["Found_end"] = true}
end
return {["paths"]=paths,["moves"]=moves,["Rotations"]=Rotations,["plrpos"]=plrpos,["plrRot"]=plrRot, ["Found_end"] = true}
end
else
for _,v in pairs(directions) do
local v_string=reversetbl(directions)[tostring(v)]
if plrpos+v==targetblock[1] and not testforwall(v_string) then
if contains(oposeingdirections[plrRot],v_string) then
return {["paths"]=paths,["moves"]=moves,["Rotations"]=Rotations+1,["plrpos"]=plrpos,["plrRot"]=v_string, ["Found_end"] = true}
else
if v_string==oposeingdirections2[plrRot] then
return {["paths"]=paths,["moves"]=moves,["Rotations"]=Rotations+2,["plrpos"]=plrpos,["plrRot"]=v_string, ["Found_end"] = true}
end
return {["paths"]=paths,["moves"]=moves,["Rotations"]=Rotations,["plrpos"]=plrpos,["plrRot"]=plrRot, ["Found_end"] = true}
end
end
end
end
end
local function checkoposeingangles()-----------------------------function that looks left and right to see if there are no walls
for _,v in ipairs(oposeingdirections[plrRot]) do
if not testforwall(v) and not eliminated_blocks[plrpos+directions[tostring(v)]] then
local newpathtable = {
{plrpos + directions[v], v},
moves + 1,
Rotations + 1,
copyTable(eliminated_blocks)
}
newpathtable[4][plrpos] = true -- correctly eliminate the turn block
table.insert(paths, newpathtable)
end
end
end
local counter=0
while not testforwall(plrRot) and not eliminated_blocks[plrpos+directions[plrRot]] do-----------------loop
local ifwon = lookforwinningblock()-------------------looks for winning block
if ifwon then
return ifwon
end
checkoposeingangles()
---------------------------looks behind if its the first square
if counter==0 and lookback then
if not testforwall(oposeingdirections2[plrRot]) and not eliminated_blocks[plrpos+directions[tostring(oposeingdirections2[plrRot])]] then
local newpathtable = {
{plrpos + directions[oposeingdirections2[plrRot]], oposeingdirections2[plrRot]},
moves + 1,
Rotations + 1,
copyTable(eliminated_blocks)
}
newpathtable[4][plrpos] = true -- correctly eliminate the turn block
table.insert(paths, newpathtable)
end
end
counter=counter+1
-------------------------------------------------------------------------------------------------------for testing
-----------------if this is set to true it prints a grid of all the eliminated blocks. this shows the path that was done and was used for debuging
if false then
emu.yield();
printGrid(eliminated_blocks,targetblock[1])
emu.yield();
end
------------------------------------------------------------------------------------------------------------------
eliminated_blocks[plrpos]=true
plrpos=plrpos+directions[plrRot]
moves=moves+1
end
eliminated_blocks[plrpos]=true
checkoposeingangles()
local ifwon = lookforwinningblock()
if ifwon then
return ifwon
end
return {["eliminated_blocks"]=eliminated_blocks,["paths"]=paths,["moves"]=moves,["Rotations"]=Rotations,["plrpos"]=plrpos,["plrRot"]=plrRot, ["Found_end"] = false}------------------------returns all the data
then I use for loops inside of for loops to look through all the paths. then make paths for those paths, and paths for those paths.. so on
---------this sees if the end was found through the path it just finished
---------and if it was it checks to see if its better then the current best lowest number of movements
local function checkforbest(info)
if info["Found_end"]==true then
if info["moves"] < best["Moves"] then
best = {["Moves"] = info["moves"], ["Rots"] = info["Rotations"]}
endposofbest={info["plrpos"],info["plrRot"]}
else
if info["moves"] == best["Moves"] then
if info["Rotations"] < best["Rots"] then
best = {["Moves"] = info["moves"], ["Rots"] = info["Rotations"]}
endposofbest={info["plrpos"],info["plrRot"]}
end
end
end
end
end
local info=GoStright(start,{},0,0,true)
checkforbest(info)
for _,path in ipairs(info["paths"]) do
local info2=GoStright({path[1][1],path[1][2]},path[4],path[2],path[3])
checkforbest(info2)
for _,path2 in ipairs(info2["paths"]) do
local info3=GoStright({path2[1][1],path2[1][2]},path2[4],path2[2],path2[3])
checkforbest(info3)
for _,path3 in ipairs(info3["paths"]) do
local info4=GoStright({path3[1][1],path3[1][2]},path3[4],path3[2],path3[3])
checkforbest(info4)
for _,path4 in ipairs(info4["paths"]) do
local info5=GoStright({path4[1][1],path4[1][2]},path4[4],path4[2],path4[3])
checkforbest(info5)
for _,path5 in ipairs(info5["paths"]) do
local info6=GoStright({path5[1][1],path5[1][2]},path5[4],path5[2],path5[3])
checkforbest(info6)
for _,path6 in ipairs(info6["paths"]) do
local info7=GoStright({path6[1][1],path6[1][2]},path6[4],path6[2],path6[3])
checkforbest(info7)
for _,path7 in ipairs(info7["paths"]) do
local info8=GoStright({path7[1][1],path7[1][2]},path7[4],path7[2],path7[3])
checkforbest(info8)
for _,path8 in ipairs(info8["paths"]) do
local info9=GoStright({path8[1][1],path8[1][2]},path8[4],path8[2],path8[3])
checkforbest(info9)
-----------------------------------------this does mean that the script only can make a maximum of 9 turns but thats more then enough to get from 1 point to another
end
end
end
end
end
end
end
end
and i think that's just about everything in terms of explaining my script. if you would like to look at the full script go to this pastebin link :
https://pastebin.com/1WAyExgP if you cant see the whole script through that link and you would like to just ask me for it, I because don't have like a GitHub or anything and I don't feel like making one