User File #638643807847186809

Upload All User Files

#638643807847186809 - Data-oriented savestate manipulation in Lua, as used in 6203M (Kwirk "Heading Out? all floors")

tas.lua
Game: Kwirk ( GB, see all files )
15 downloads
Uploaded 10/13/2024 1:46 AM by Sand (see all 4)
Lua script that plays Kwirk "Heading Out? all floors" from beginning to end. This is the same script that was used for [6203] GB Kwirk "Heading Out?, all floors" by Sand in 25:15.19, with some additional comments and edits for clarity. The reason for posting this is to demonstrate the savestate manipulation support code included in the script (from BASE_SAVESTATE to earliest_effective_frame). See Forum/Topics/25755.
This is tas.lua in the Git repository at https://www.bamsoftware.com/git/kwirk.git at commit ef2355417be15e9473ff6d776a3e85247f58db5f.
-- ./EmuHawkMono.sh --lua=Lua/tas.lua kwirk.gb

SOLUTIONS = {
	-- Easy
	[ "30" ] = { frames =  222, steps =  24, moves = "WWWWWWNWSWNWSWNWSWWWWWWW" },
	[ "30i"] = { frames =  240, steps =  26, moves = "WWSWWWWNWSWNWSWNWSWWWWNWWW" },
	[ "31" ] = { frames =  429, steps =  44, moves = "WWWWWNWWWWWWSSWSEEEEESENNNENWWWWWWNWSSWWWWWW" },
	[ "31i"] = { frames =  447, steps =  46, moves = "WWSWWWWSWWWWWNNWNEEEEENESSSESWWWWWWSWNNWWWNWWW" },
	[ "32" ] = { frames =  228, steps =  24, moves = "WWWWWWNWWWSENWWWSWWWWWWW" },
	[ "32i"] = { frames =  246, steps =  26, moves = "WWSWWWWNWWWSENWWWSWWWWNWWW" },
	[ "33" ] = { frames =  252, steps =  26, moves = "WWNWWWWSWWNNWWSSWWNWWWSWWW" },
	[ "33i"] = { frames =  270, steps =  28, moves = "WWSSWWWWSWWNNWWSSWWNWWWNNWWW" },
	[ "34" ] = { frames =  381, steps =  41, moves = "WWWWWWWWWWSEEEEENNWWWWWWNEEEESWWWWWWWWWWW" },
	[ "34i"] = { frames =  399, steps =  43, moves = "WWSWWWWWWWWNEEEEESSWWWWWWSEEEENWWWWWWWWNWWW" },
	[ "35" ] = { frames =  383, steps =  42, moves = "WWSSSWWWWWNNEWNENWNWWWWWSSSEESSWWWWWNNNWWW" },
	[ "35i"] = { frames =  365, steps =  40, moves = "WWNNWWWWWSSEWSESWSWWWWWNNNEENNWWWWWSSWWW" },
	[ "36" ] = { frames =  257, steps =  28, moves = "WWWWWSSSWWWWWWNENNNWSWWWWWWW" },
	[ "36i"] = { frames =  275, steps =  30, moves = "WWSWWWNNNWWWWWWSESSSWNWWWWNWWW" },
	[ "37" ] = { frames =  247, steps =  27, moves = "WWWWWWNNWSENNWWWSSWWWWWWWWW" },
	[ "37i"] = { frames =  265, steps =  29, moves = "WWSWWWWSSWNESSWWWNNWWWWWWNWWW" },
	[ "38" ] = { frames =  330, steps =  34, moves = "WWNNWWWSSSSSWNNENNNWWWWWWWWWWSSWWW" },
	[ "38i"] = { frames =  348, steps =  36, moves = "WWSSSWWWNNNNNWSSESSSWWWWWWWWWWNNNWWW" },
	[ "39" ] = { frames =  288, steps =  30, moves = "WWNNWWWWSSWWSSWNNWWNNWWWWSSWWW" },
	[ "39i"] = { frames =  306, steps =  32, moves = "WWSSSWWWWNNWWNNWSSWWSSWWWWNNNWWW" },
	[ "40" ] = { frames =  273, steps =  30, moves = "WWSWWWWWNNWWEESSSWWNWWWWWWNWWW" },
	[ "40i"] = { frames =  255, steps =  28, moves = "WWWWWWWSSWWEENNNWWSWWWWWWWWW" },
	[ "41" ] = { frames =  369, steps =  40, moves = "WWWWWNNWWWWWSWEENEEEESSSSWWWWWNWWNWWWWWW" },
	[ "41i"] = { frames =  387, steps =  42, moves = "WWSWWWSSWWWWWNWESEEEEENNNNWWWWSWWWSWWWNWWW" },
	[ "42" ] = { frames =  278, steps =  30, moves = "WWWWWNNWWWWWWSENSWNESSWWWWWWWW" },
	[ "42i"] = { frames =  296, steps =  32, moves = "WWSWWWNNWWWWWWSENSWNESSWWWWWNWWW" },
	[ "43" ] = { frames =  406, steps =  42, moves = "WWNNWWWWSESWNWSESWSWWSWNENWSWNNNNWWWWSSWWW" },
	[ "43i"] = { frames =  424, steps =  44, moves = "WWSSSWWWWNENWSWNENWNWWNWSESWNWSSSSWWWWNNNWWW" },
	[ "44" ] = { frames =  363, steps =  38, moves = "WWSSSWWWWWWNESSWWNWWSSEENNNNWWWWWSSWWW" },
	[ "44i"] = { frames =  363, steps =  38, moves = "WWNNWWWWWWSENNWWSWWNNEESSSSWWWWWNNNWWW" },
	[ "45" ] = { frames =  258, steps =  28, moves = "WWWWWWWNSSSWWNENNWSWWWWWWWWW" },
	[ "45i"] = { frames =  276, steps =  30, moves = "WWSWWWWSNWNNWWSESSWNWWWWWWNWWW" },
	[ "46" ] = { frames =  260, steps =  28, moves = "WWWWWWWWENNWSWWESSWNWWWWWWWW" },
	[ "46i"] = { frames =  278, steps =  30, moves = "WWSWWWWWWESSWNWWENNWSWWWWWNWWW" },
	[ "47" ] = { frames =  328, steps =  36, moves = "WWNNWWWWSSSSWWWEENNNWSWWWNNWWWWSSWWW" },
	[ "47i"] = { frames =  346, steps =  38, moves = "WWSSSWWWWNNNNWWWEESSSWNWWWSSWWWWNNNWWW" },
	[ "48" ] = { frames =  459, steps =  50, moves = "WWNWWWWWNWWWWWSEEEEEEWWWWWSSEENEESSSWWWWWWWWNNNWWW" },
	[ "48i"] = { frames =  459, steps =  50, moves = "WWSSWWWWWSWWWWWNEEEEEEWWWWWNNEESEENNNWWWWWWWWSSWWW" },
	[ "49" ] = { frames =  345, steps =  37, moves = "WWWWWWWWEENNWSSESSWNENNNWWWSWSWWWWWWW" },
	[ "49i"] = { frames =  363, steps =  39, moves = "WWSWWWWWWEENNNWSSESSWNESSWWWWNNWWWWNWWW" },
	[ "50" ] = { frames =  296, steps =  32, moves = "WWWWWWWWNNWSESSSSWWNENWWNWWWWWWW" },
	[ "50i"] = { frames =  314, steps =  34, moves = "WWSWWWWWWSSWNENNNNWWSESWWSWWWWNWWW" },
	[ "51" ] = { frames =  324, steps =  34, moves = "WWSSSWWWWWNNNWSENWWWWSSSWWWWNNNWWW" },
	[ "51i"] = { frames =  306, steps =  32, moves = "WWNNWWWWWSSSWNESWWWWNNNWWWWSSWWW" },
	[ "52" ] = { frames =  510, steps =  54, moves = "WWNWWWSWWWWWNEEEEESSWNESSWWWNEEESSWNESSWWWWWWWNWWWSWWW" },
	[ "52i"] = { frames =  528, steps =  56, moves = "WWSSWWWNWWWWWSEEEEENNWSENNWWWSEEENNWSENNWWWWWWWSWWWNNWWW" },
	[ "53" ] = { frames =  324, steps =  34, moves = "WWNNWWWWWWWSEESSWSWNESWWWWWWNNNWWW" },
	[ "53i"] = { frames =  324, steps =  34, moves = "WWSSSWWWWWWWNEENNNWWSENWWWWWWSSWWW" },
	[ "54" ] = { frames =  293, steps =  32, moves = "WWNNWWWSSSWNWNWWWSSWSWSWWWNNNWWW" },
	[ "54i"] = { frames =  293, steps =  32, moves = "WWSSSWWWNNNWSWSWWWNNWNWNWWWSSWWW" },
	[ "55" ] = { frames =  277, steps =  30, moves = "WWSWWWWWWNNWWSENWWSSSWWWWWNWWW" },
	[ "55i"] = { frames =  259, steps =  28, moves = "WWWWWWWWSSWWNESWWNNNWWWWWWWW" },
	[ "56" ] = { frames =  300, steps =  30, moves = "WWWWWWWSWESEENWWWWNSWWWNWWWWWW" },
	[ "56i"] = { frames =  318, steps =  32, moves = "WWSWWWWWNWENEESWWWWSNWWWSWWWNWWW" },
	[ "57" ] = { frames =  272, steps =  30, moves = "WWNNWWWWWWWWSWSSESSWWWWWNNNWWW" },
	[ "57i"] = { frames =  272, steps =  30, moves = "WWSSSWWWWWWWWNWNNENNWWWWWSSWWW" },
	[ "58" ] = { frames =  495, steps =  51, moves = "WWWWWWSWWSWWWNENNEESSESWWWWEENNWSSSWWWNNNNNWWWSSWWW" },
	[ "58i"] = { frames =  513, steps =  53, moves = "WWSWWWWNWWWNWWSESSEENNENWWWWEESSWNNWNWSSSSSWWWWNNNWWW" },
	[ "59" ] = { frames =  399, steps =  43, moves = "WWSWWWWWSWWEENNWSENNWWNEEWWSENWWSSWWWWWNWWW" },
	[ "59i"] = { frames =  381, steps =  41, moves = "WWWWWWWNWWEESSWNESSWWSEEWWNESWWNNWWWWWWWW" },

	-- Average
	[ "60" ] = { frames =  479, steps =  48, moves = "WWWWWNNWWSSNNEESSSSWNENWWWWEEESSWNENWWWWWWWWWWWW" },
	[ "60i"] = { frames =  497, steps =  50, moves = "WWSWWWNNWWSSNNEESSSSWNENWWWWEEESSWNENWWWWWWWWWNWWW" },
	[ "61" ] = { frames =  435, steps =  46, moves = "WWWWWWNWWWNEESESESNNWWNWWSEEESSSSWWNWWWNWWWWWW" },
	[ "61i"] = { frames =  453, steps =  48, moves = "WWSWWWWSWWWSEENNEENSSWWSWWNEEENNNNWWSWWWSWWWNWWW" },
	[ "62" ] = { frames =  585, steps =  63, moves = "WWSWWWNWWNEESSWWWSWENEEENNWWSEENNWWWWSSESEEENNNNWWWWSWWWWWWSWWW" },
	[ "62i"] = { frames =  585, steps =  63, moves = "WWWWWWSWSEENNWWWNWESEEESSWWNEESSWWWWNNENEEESSSSWWWWNWWWWWWNNWWW" },
	[ "63" ] = { frames =  476, steps =  52, moves = "WWNNWWWWSSSSSWNNNNWWNWWSSSNNNEESWSSEESSWWWWWWWNNNWWW" },
	[ "63i"] = { frames =  476, steps =  52, moves = "WWSSSWWWWNNNNNWSSSSWWSWWNNNSSSEENWNNEENNWWWWWWWSSWWW" },
	[ "64" ] = { frames =  328, steps =  35, moves = "WWNNWWWWSWWWEEESSSWNWWSWWWWWWNNNWWW" },
	[ "64i"] = { frames =  328, steps =  35, moves = "WWSSSWWWWNWWWEEENNNWSWWNWWWWWWSSWWW" },
	[ "65" ] = { frames =  342, steps =  36, moves = "WWWWWWWNNWWSEWSENWWNEESSSWWNWWWWWWWW" },
	[ "65i"] = { frames =  360, steps =  38, moves = "WWSWWWWWSSWWNEWNESWWSEENNNWSWWWWWWNWWW" },
	[ "66" ] = { frames =  246, steps =  26, moves = "WWSWWWWWWWWNEWWNESWWWWWWWW" },
	[ "66i"] = { frames =  246, steps =  26, moves = "WWWWWWWWWWSEWWSENWWWWWNWWW" },
	[ "67" ] = { frames =  317, steps =  34, moves = "WWNNWWWSWSSWSWWSWWNNEENNWWWWWSSWWW" },
	[ "67i"] = { frames =  335, steps =  36, moves = "WWSSSWWWNWNNWNWWNWWSSEESSWWWWWNNNWWW" },
	[ "68" ] = { frames =  243, steps =  26, moves = "WWWWWWNWWSWNWWESSWNWWWWWWW" },
	[ "68i"] = { frames =  261, steps =  28, moves = "WWSWWWWSWWNWSWWENNWSWWWWNWWW" },
	[ "69" ] = { frames =  320, steps =  34, moves = "WWNWWWWSWNSWSWNEENENWWWWSWWWWWSWWW" },
	[ "69i"] = { frames =  338, steps =  36, moves = "WWSSWWWNWWSNWNWSEESESWWWWNWWWWWNNWWW" },
	[ "70" ] = { frames =  393, steps =  42, moves = "WWNNWWWSSSSSWWNENENNWSNNWWWWWSSSWWWWNNNWWW" },
	[ "70i"] = { frames =  393, steps =  42, moves = "WWSSSWWWNNNNNWWSEESSSWNSSWWWWWNNWNWWWSSWWW" },
	[ "71" ] = { frames =  382, steps =  42, moves = "WWNNWWWSSSSWWNNENNWWWSSESSWWWNNNNWWWWSSWWW" },
	[ "71i"] = { frames =  400, steps =  44, moves = "WWSSSWWWNNNNWWSSESSWWWNNENNWWWSSSSWWWWNNNWWW" },
	[ "72" ] = { frames =  360, steps =  39, moves = "WWSWWWWWSWWEENNNNWWSESSWNESSWWWWWWWNWWW" },
	[ "72i"] = { frames =  342, steps =  37, moves = "WWWWWWWNWWEESSSSWWNENNWSENNWWWWWWWWWW" },
	[ "73" ] = { frames =  353, steps =  36, moves = "WWWWWWNWWSENENWWWEESSWNNWWSSWWWWWWWW" },
	[ "73i"] = { frames =  371, steps =  38, moves = "WWSWWWWSWWNESESWWWEENNWSSWWNNWWWWWNWWW" },
	[ "74" ] = { frames =  429, steps =  43, moves = "WWWWWSSWSWWWNNESWNNWWNEEENEESSWWWWWWWWWWWWW" },
	[ "74i"] = { frames =  447, steps =  45, moves = "WWSWWWNNNWWWWSSENWSSWWSEEESEENNWWWWWWWWWWNWWW" },
	[ "75" ] = { frames =  390, steps =  42, moves = "WWNNWWWWWWWWSSEEESENNNWWWWSSESWSWWWWNNNWWW" },
	[ "75i"] = { frames =  390, steps =  42, moves = "WWSSSWWWWWWWWNNEEENESSSWWWWNNENWNWWWWSSWWW" },
	[ "76" ] = { frames =  238, steps =  26, moves = "WWWWWWWSWNENNWWWSSWWWWWWWW" },
	[ "76i"] = { frames =  256, steps =  28, moves = "WWSWWWWWNWSESSWWWNNWWWWWNWWW" },
	[ "77" ] = { frames =  306, steps =  33, moves = "WWWWWWSSWWNNNEESSSSWWWWNWWNWWWWWW" },
	[ "77i"] = { frames =  324, steps =  35, moves = "WWSWWWNNWWWSSSEENNNNWWWWWSWSWWWNWWW" },
	[ "78" ] = { frames =  282, steps =  30, moves = "WWWWWSSSWWWEENNWWNWWWSWWWWNWWW" },
	[ "78i"] = { frames =  282, steps =  30, moves = "WWSWWWNNNWWWEESSWWSWWWNWWWWWWW" },
	[ "79" ] = { frames =  314, steps =  34, moves = "WWWWWWNNWWWEEESSSSWNWWWSWNNWWWWWWW" },
	[ "79i"] = { frames =  332, steps =  36, moves = "WWSWWWWSSWWWEEENNNNWSWWWNWSSWWWWNWWW" },
	[ "80" ] = { frames =  314, steps =  30, moves = "WWNNWWWSSSWWWNWNWWSSWSWWWNNWWW" },
	[ "80i"] = { frames =  314, steps =  30, moves = "WWSSSWWWNNNWWWSWSWWNNWNWWWSWWW" },
	[ "81" ] = { frames =  507, steps =  54, moves = "WWNNWWWSSSWSSWWNWSEENNEENNNWWSSSSWWNESWWNNWSSWWWNNNWWW" },
	[ "81i"] = { frames =  507, steps =  54, moves = "WWSSSWWWNNNWNNWWSWNEESSEESSSWWNNNNWWSENWWSSWNNWWWSSWWW" },
	[ "82" ] = { frames =  276, steps =  30, moves = "WWWWWWNNWWWEESSWNWWWSSWWWWNWWW" },
	[ "82i"] = { frames =  276, steps =  30, moves = "WWSWWWWSSWWWEENNWSWWWNNWWWWWWW" },
	[ "83" ] = { frames =  259, steps =  28, moves = "WWWWWWWNWESSWNWNSSWWWNWWWWWW" },
	[ "83i"] = { frames =  277, steps =  30, moves = "WWSWWWWWNWESSWNWNSSWWWNWWWNWWW" },
	[ "84" ] = { frames =  371, steps =  36, moves = "WWWWWWWWWSNNESEESSWWNNENWWWWSWWWWWWW" },
	[ "84i"] = { frames =  389, steps =  38, moves = "WWSWWWWWWWSNNESEESSWWNNENWWWWSWWWWNWWW" },
	[ "85" ] = { frames =  353, steps =  36, moves = "WWWWWSSWWNWWWEEENEENNWWSWWWSWWWWWWWW" },
	[ "85i"] = { frames =  371, steps =  38, moves = "WWSWWWSSWWNWWWEEENEENNWWSWWWSWWWWWNWWW" },
	[ "86" ] = { frames = 1218, steps = 128, moves = "WWNNWWWWSSWNESSWWWNEEESSWNESSWWWWWNEEEEESSWNESSWWWNEEESSWNESSSSWNESSWWWNEEESSWNESSWWWWWNEEEEESSWNESSWWWNEEESSWNESSWWWWWWWWWNNWWW" },
	[ "86i"] = { frames = 1218, steps = 128, moves = "WWSSSWWWWNNWSENNWWWSEEENNWSENNWWWWWSEEEEENNWSENNWWWSEEENNWSENNNNWSENNWWWSEEENNWSENNWWWWWSEEEEENNWSENNWWWSEEENNWSENNWWWWWWWWWSWWW" },
	[ "87" ] = { frames =  486, steps =  52, moves = "WWWWWNNWWEESSSSWWEENNWSNNWSENNWWWSWWNESWWNNWSSWWWWWW" },
	[ "87i"] = { frames =  504, steps =  54, moves = "WWSWWWNNWWEESSSSWWEENNWNSSWNESSWWWNWWSENWWSSWNNWWWNWWW" },
	[ "88" ] = { frames =  884, steps =  90, moves = "WWNNWWWWWSSNNWWSSSSSWWWNNEEWWSSEEENNNNSSSSEENENWWENNNWWWEEESSSSSWWNNNNSSEENNNWWWWWWWWSSWWW" },
	[ "88i"] = { frames =  902, steps =  92, moves = "WWSSSWWWWWNNSSWWNNNNNEESESWWENNWWSSSSNNEESSSWWWENNNNNWWWSSEEWWNNEEESSSSNNEESSSWWWWWWWWNNNWWW" },
	[ "89" ] = { frames =  577, steps =  60, moves = "WWNNWWWSWWWSWWNEEESEENNWSESWWWWNWSSSWSEEENEESWWWWWWWWWNNNWWW" },
	[ "89i"] = { frames =  577, steps =  60, moves = "WWSSSWWWNWWWNWWSEEENEESSWNENWWWWSWNNNWNEEESEENWWWWWWWWWSSWWW" },
	[ "90" ] = { frames =  388, steps =  41, moves = "WWNNWWWWSSSSSWWWNWSEEENENNWWSWWNWWWWSSWWW" },
	[ "90i"] = { frames =  406, steps =  43, moves = "WWSSSWWWWNNNNNWWSWWNEESEESSWWNWWSWWWWNNNWWW" },
	[ "91" ] = { frames =  604, steps =  62, moves = "WWWWWNNWWWWWWSSESSSEEEENNWEENNWNWSSNEESSWWWWESSWWNNENWWWWWWWWW" },
	[ "91i"] = { frames =  622, steps =  64, moves = "WWSWWWSWSWWWWWNNENNNEEEESSWEESSSWWNNSEENNWWWWENNWWSSESWWWWWWNWWW" },
	[ "92" ] = { frames =  483, steps =  46, moves = "WWWWWNNWWSSWSWSEEENENWWWWWSESEENENWWWWWWWWWWWW" },
	[ "92i"] = { frames =  501, steps =  48, moves = "WWSWWWNNWWSSWSWSEEENENWWWWWESESENENWWWWWWWWWNWWW" },
	[ "93" ] = { frames =  296, steps =  31, moves = "WWSWWWWWSSWWNENNWWNESWWSWWWNWWW" },
	[ "93i"] = { frames =  278, steps =  29, moves = "WWWWWWWNNWWSESSWWSENWWNWWWWWW" },
	[ "94" ] = { frames =  320, steps =  34, moves = "WWWWWWNWWEESSWWNWSWENNWSWNWSWWWWWW" },
	[ "94i"] = { frames =  338, steps =  36, moves = "WWSWWWWNWWEESSWWNWSWENNWSWNWSWWWNWWW" },
	[ "95" ] = { frames =  319, steps =  34, moves = "WWWWWNNWWSESNNWWSSWWSSWSWNNNWWWWWW" },
	[ "95i"] = { frames =  337, steps =  36, moves = "WWSWWWSSWWNENSSWWNNWWNNWNWSSSWWWNWWW" },
	[ "96" ] = { frames =  502, steps =  51, moves = "WWNNWWWWSSWESNNNWWWWSSENSWWWSNNNEEESSWSSWWWWWNNNWWW" },
	[ "96i"] = { frames =  502, steps =  51, moves = "WWSSSWWWWNNWENSSSWWWWNNESNWWWNSSSEEENNWNNWWWWWSSWWW" },
	[ "97" ] = { frames =  365, steps =  38, moves = "WWNNWWWSSSWWSWWNNNEENNWSENNWWWWWWSSWWW" },
	[ "97i"] = { frames =  383, steps =  40, moves = "WWSSSWWWNNNWWNWWSSSEESSWNESSWWWWWWNNNWWW" },
	[ "98" ] = { frames =  370, steps =  40, moves = "WWWWWWNNWWSESESSWNNNSSSWWWNENNWSWWWWWWWW" },
	[ "98i"] = { frames =  388, steps =  42, moves = "WWSWWWWSSWWNENENNWSSSNNWNWWSESSWNWWWWWNWWW" },
	[ "99" ] = { frames =  652, steps =  66, moves = "WWNNWWWSWSWSWWWNNWSESSWWNEEEESENNWNEENESSSWSWWWWSWNNNENWWNWWWSSWWW" },
	[ "99i"] = { frames =  670, steps =  68, moves = "WWSSSWWWNWNWNWWWSSWNENNWWSEEEENESSWSEESENNNWNWWWWNWSSSESWWSWWWNNNWWW" },
	["100" ] = { frames =  499, steps =  48, moves = "WWWWWNWNWSSNWEEESWWWNWSESWENEESWWWWWWSSWWWNNNWWW" },
	["100i"] = { frames =  499, steps =  48, moves = "WWSWWWSWSWWENNSEENWWWSWNENWSEEENWWWWWWNNWWWSSWWW" },
	["101" ] = { frames =  873, steps =  82, moves = "WWNNWWWSSWWSWWNWESEENWWWWEESWESSWNESENNENWWWWEESWWWNSEESSEEENNENWWWWWSWNNNWWWSSWWW" },
	["101i"] = { frames =  891, steps =  84, moves = "WWSSSWWWNNWWNWWSWENEESWWWWEENWENNWSENESSESWWWWEENWWWSNEEENNEESSESWWWWWNWSSSWWWNNNWWW" },
	["102" ] = { frames =  755, steps =  74, moves = "WWSSSWWWNNNWWSSEENNNWWSEWWWNNEEEESSSWWWWNWWNNWSSEEWWSSENENWNWSSSSWWWNNNWWW" },
	["102i"] = { frames =  737, steps =  72, moves = "WWNNWWWSSSWWNNEESSSWWNEWWWSSEEEENNNWWWWSWWSSWNNEEWWNNESESWSWNNNNWWWSSWWW" },
	["103" ] = { frames =  692, steps =  73, moves = "WWSSSWWWWWWWNNNEEENNWWWSSWWSSSEENNSSWWNNNEEWWNSSSSEENNNNEEESSWWWWWWWWWWWW" },
	["103i"] = { frames =  692, steps =  73, moves = "WWNNWWWWWWWSSSEEESSWWWNNWWNNNEESSNNWWSSSEEWWSNNNNEESSSSEEENNWWWWWWWWWNWWW" },
	["104" ] = { frames =  600, steps =  60, moves = "WWSSSWWWNWWWWWSWWNEEEEESEENWSWNNNWWWEEENNWSESWWWWWNNWWWSSWWW" },
	["104i"] = { frames =  600, steps =  60, moves = "WWNNWWWSWWWWWNWWSEEEEENEESWNWSSSWWWEEESSWNENWWWWWSSWWWNNNWWW" },
	["105" ] = { frames =  423, steps =  43, moves = "WWSWWWNNWWWSNWWSSWEEESEESNWWSNWWSWWNWWWNWWW" },
	["105i"] = { frames =  405, steps =  41, moves = "WWWWWSSWWWNNWENEENSWWNNWWSWESSSNWWNWWWWWW" },
	["106" ] = { frames =  464, steps =  50, moves = "WWNNWWWSWSWSWNESSWWSWWEENEENNWNWWSSSWNESSWWWWWNWWW" },
	["106i"] = { frames =  464, steps =  50, moves = "WWSSSWWWNWNWNWSENNWWNWWEESEESSSWWWNNNWSENNWWWWWWWW" },
	["107" ] = { frames =  544, steps =  58, moves = "WWSWWWWSNNWWWNNWWWSENEEEEESSWNESSWWWNSESWWNESSWWSWWWWNNWWW" },
	["107i"] = { frames =  526, steps =  56, moves = "WWWWWWNSSWWWSSWWWNESEEEEENNWSENNWWWSNENWWSENNWWNWWWWSWWW" },
	["108" ] = { frames =  314, steps =  34, moves = "WWSWWWWSWWWEEENNNWWSWNSSWNWWWWWWWW" },
	["108i"] = { frames =  314, steps =  34, moves = "WWWWWWNWWWEEESSSWWNWSNNWSWWWWWNWWW" },
	["109" ] = { frames = 1083, steps = 115, moves = "WWWWWWWWWEEEESSWNESSWWWNEEESSWNESSWWWWWEEEEENNWSENNWWWSEEENNWSENNWWWWWSWENEEEEENNWSENNWWWSEEENNWSENNWWWWWSWSWWWWWWW" },
	["109i"] = { frames = 1101, steps = 117, moves = "WWSWWWWWWWEEEESSWNESSWWWNEEESSWNESSWWWWWEEEEENNWSENNWWWSEEENNWSENNWWWWWSWENEEEEENNWSENNWWWSEEENNWSENNWWWWWSWSWWWWNWWW" },

	-- Hard
	["110" ] = { frames =  502, steps =  46, moves = "WWWWWNWNWWWSSSSSWWNESENSEENWWWSWNENWSWNNWWWWWW" },
	["110i"] = { frames =  520, steps =  48, moves = "WWSWWWSWSWWWNNNNNWWSENESNEESWWWNWSESWNWSSWWWNWWW" },
	["111" ] = { frames =  551, steps =  55, moves = "WWWWWWSWWSWEEESWWNNEENNWWSSSWWWENENNNWWWSESSENNWWWWWWWW" },
	["111i"] = { frames =  569, steps =  57, moves = "WWSWWWWNWWNWNESEESSSWNNENWSWNWWWESESSSWWWNENNESSWWWWWNWWW" },
	["112" ] = { frames =  344, steps =  36, moves = "WWWWWWNNWWESESSWNNENWWWSSSWWNWWWWWWW" },
	["112i"] = { frames =  362, steps =  38, moves = "WWSWWWWNNWWESESSWNNENWWWSSSWWNWWWWNWWW" },
	["113" ] = { frames =  542, steps =  57, moves = "WWSWWWNWWWWSEEESSWNNWWSWWSSEEENNNEENNWSESSWWNNNWWSSWWWWWW" },
	["113i"] = { frames =  542, steps =  57, moves = "WWWWWSWWWWNEEENNWSSWWNWWNNEEESSSEESSWNNENWWSSSWWNNWWWNWWW" },
	["114" ] = { frames =  351, steps =  32, moves = "WWSWWWNNWWSWWNWWSESWSSWNNWWWNWWW" },
	["114i"] = { frames =  333, steps =  30, moves = "WWWWWSSWWNWWSWWNENWNNWSSWWWWWW" },
	["115" ] = { frames =  578, steps =  58, moves = "WWSSSWWWNNWWWEEESSWNENWWWWNWSEEEESSWNENWWWSWNNNNWWWWWSSWWW" },
	["115i"] = { frames =  578, steps =  58, moves = "WWNNWWWSSWWWEEENNWSESWWWWSWNEEEENNWSESWWWNWSSSSWWWWWNNNWWW" },
	["116" ] = { frames =  466, steps =  49, moves = "WWWWWSSSWWWWWNSEEEEENNNNNWWWSSWWWNEEESSWWWWWWWWWW" },
	["116i"] = { frames =  484, steps =  51, moves = "WWSWWWNNNWWWWWSNEEEEESSSSSWWWNNWWWSEEENNWWWWWWWNWWW" },
	["117" ] = { frames =  474, steps =  49, moves = "WWNNWWWWWSEESWWSNWWSNNNWWSSSENWWNNEESSSWWNWWWNWWW" },
	["117i"] = { frames =  474, steps =  49, moves = "WWSSSWWWWWNEENWWNSWWNSSSWWNNNESWWSSEENNNWWSWWWWWW" },
	["118" ] = { frames =  445, steps =  46, moves = "WWSWWWWSWWWWEEEENESWWWWWENNNNWESSWSNNWWSWWWWWW" },
	["118i"] = { frames =  445, steps =  46, moves = "WWWWWWNWWWWEEEESENWWWWWESSSSWENNWNSSWWNWWWNWWW" },
	["119" ] = { frames =  545, steps =  58, moves = "WWNNWWWWWWSSNNWWWSENEEESWNEESSENNSSWNESSWWWSSWWWWWWWNNNWWW" },
	["119i"] = { frames =  545, steps =  58, moves = "WWSSSWWWWWWNNSSEENWSWWWNESEEENNESSNNWSENNWWWNNWWWWWWWSSWWW" },
	["120" ] = { frames =  333, steps =  32, moves = "WWWWWSSSWWWNNNSEENNWWWWWWSWWWWWW" },
	["120i"] = { frames =  351, steps =  34, moves = "WWSWWWNNNWWWSSSNEESSWWWWWWNWWWNWWW" },
	["121" ] = { frames =  629, steps =  58, moves = "WWNNWWWWWWWWSWSENEEEEESSSSWNNENNWWWWNWSESWNWSSSWSWWWNNNWWW" },
	["121i"] = { frames =  629, steps =  58, moves = "WWSSSWWWWWWWWNWNESEEEEENNNNWSSESSWWWWSWNENWSWNNNWNWWWSSWWW" },
	["122" ] = { frames =  462, steps =  48, moves = "WWWWWWWWNWWNWESESESSESEENWWSWNNSEENNWWWWWWWWWWWW" },
	["122i"] = { frames =  480, steps =  50, moves = "WWSWWWWWWSWWSWENENENNENEESWWNWSSNEESSWWWWWWWWWNWWW" },
	["123" ] = { frames =  624, steps =  64, moves = "WWSSSWWWNNSSWWNNNESSSSWWNNWSEENNNWWSSSWSENEESSWNENWWWWSWNNWWWWWW" },
	["123i"] = { frames =  624, steps =  64, moves = "WWNNWWWSSNNWWSSSENNNNWWSSWNEESSSWWNNNWNESEENNWSESWWWWNWSSWWWNWWW" },
	["124" ] = { frames =  624, steps =  62, moves = "WWWWWSSSWWWWWNSEENWSEENNNNNWESEESSWWEESSWWWNNNNENWWWWWSSWWWWWW" },
	["124i"] = { frames =  642, steps =  64, moves = "WWSWWWNNNWWWWWSNEESWNEESSSSSWENEENNWWEENNWWWSSSSESWWWWWNNWWWNWWW" },
	["125" ] = { frames = 1207, steps = 118, moves = "WWWWWNNWWWSSESWNNNESSNNEESSWWEESSSWNNWWWEENNNWSESSWWWEENNNEESESSWWWWWWEEEEESSWNENWWWWWNWSEEEEESSWNENWWWWNWSSSWWWNNNWWW" },
	["125i"] = { frames = 1207, steps = 118, moves = "WWSWWWSSWWWNNENWSSSENNSSEENNWWEENNNWSSWWWEESSSWNENNWWWEESSSEENENNWWWWWWEEEEENNWSESWWWWWSWNEEEEENNWSESWWWWSWNNNWWWSSWWW" },
	["126" ] = { frames =  534, steps =  36, moves = "WWWWWNNWWWWWWSSSNNNEESSSSWWWNNWWWWWW" },
	["126i"] = { frames =  552, steps =  38, moves = "WWSWWWSSWWWWWWNNNSSSEENNNNWWWSSWWWNWWW" },
	["127" ] = { frames =  595, steps =  60, moves = "WWWWWSSSWWNWWNWSENEEENNNWSWWSWEEEESWSSEENWSWNNENWWWWWWWWWWWW" },
	["127i"] = { frames =  613, steps =  62, moves = "WWSWWWNNNWWSWWSWNSEEEESSSWNWWNWEEEENWNNEESWNWSSESWWWWWWWWWNWWW" },
	["128" ] = { frames =  765, steps =  71, moves = "WWNNWWWSWWNWWSSSEEENNWSSWWWSWNNNEEENESSESSESWWWNENESSESWWWWWWWWWWNNNWWW" },
	["128i"] = { frames =  765, steps =  71, moves = "WWSSSWWWNWWSWWNNNEEESSWNNWWWNWSSSEEESENNENNENWWWSESENNENWWWWWWWWWWSSWWW" },
	["129" ] = { frames =  601, steps =  60, moves = "WWWWWNWWWWWWSENEEEESESSWWWWSWWNEEEESENNNENWWWWWSWNNWWWWSSWWW" },
	["129i"] = { frames =  619, steps =  62, moves = "WWSWWWSWWWWNWSEEEENNENWWWWNWWSEEEENESSSESWWWWNWSESWWWWWWNNNWWW" },
	["130" ] = { frames =  611, steps =  56, moves = "WWWWWNNWWWWSSNNEESSWWWSEESSEENENWWWWWEESSWWNNENWWWWWWWWW" },
	["130i"] = { frames =  629, steps =  58, moves = "WWSWWWSSWWWWNNSSEENNWWWNEENNEESESWWWWWWWNNEESSESWWWWWWNWWW" },
	["131" ] = { frames =  318, steps =  34, moves = "WWSWWWNWNWSWSSESWWWNENNWWWSWWWNWWW" },
	["131i"] = { frames =  300, steps =  32, moves = "WWWWWSWSWNWNNENWWWSESSWWWNWWWWWW" },
	["132" ] = { frames =  518, steps =  48, moves = "WWWWWWSWWWEENENWNWWSSNNEESSWSWWNNESESWWWWWWWNWWW" },
	["132i"] = { frames =  518, steps =  48, moves = "WWSWWWWNWWWEESESWSWWNNSSEENNWNWWSSENENWWWWWWWWWW" },
	["133" ] = { frames =  811, steps =  81, moves = "WWWWWWSSSWWWWWNENEESSEENWSEENNNWWNESNWSENNWWWSWWEESESWNNNWWSSSSEENNWNNWWSSWWWWWWW" },
	["133i"] = { frames =  829, steps =  83, moves = "WWSWWWWNNNWWWWWESSEENNEESWNEESSSWWSENSWNESSWWWNWWEENENWSSSWWNNNNEESSWSSWWNNWWWWNWWW" },
	["134" ] = { frames =  597, steps =  60, moves = "WWWWWSSSWWWNNEWWNESEEENWWSWNENWWSSSENENENWSWNENWWWWWSSWWWWWW" },
	["134i"] = { frames =  615, steps =  62, moves = "WWSWWWNNNWWWSSEWWSENEEESWWNWSESWWNNNESESESWNWSESWWWWWNNWWWNWWW" },
	["135" ] = { frames =  665, steps =  68, moves = "WWNNWWWSWWWNWWSEENESWSSSNNENEESSWENNWSSSSWWNNENENWSWNENWWWWWWWWSSWWW" },
	["135i"] = { frames =  683, steps =  70, moves = "WWSSSWWWNWWWSWWNEESENWNNNSSESEENNWESSWNNNNWWSSESESWNWSESWWWWWWWWNNNWWW" },
	["136" ] = { frames =  741, steps =  76, moves = "WWSSSWWWWWWWWNNWWNNENEESEWWSWWSENENNWSSSNNEEEEEESSWNENWWWWWNWSSSSSWWWWNNNWWW" },
	["136i"] = { frames =  723, steps =  74, moves = "WWNNWWWWWWWWSSWWSSESEENEWWNWWNESESSWNNNSSEEEEEENNWSESWWWWWSWNNNNNWWWWSSWWW" },
	["137" ] = { frames = 1079, steps = 109, moves = "WWSSSWWWNNNNNWWWSESSSSWNENNNEESSSSWESWWWWWWEEEENNWSENNNENNWWSSENENNWWSWNEEWWSENWWSSSNNNWSEENEEESWWWWWWSWWWWWW" },
	["137i"] = { frames = 1079, steps = 109, moves = "WWNNWWWSSSSSWWWNENNNNWSESSSEENNNNWENWWWWWWEEEESSWNESSSESSWWNNESESSWWNWSEEWWNESWWNNNSSSWNEESEEENWWWWWWNWWWNWWW" },
	["138" ] = { frames =  671, steps =  70, moves = "WWSSSWWWWWNNNNENWSSSSSEENWSWNNNNESWSSWWWNEEEWWWSENSWNEEWWSENNWWWWWWWWW" },
	["138i"] = { frames =  671, steps =  70, moves = "WWNNWWWWWSSSSESWNNNNNEESWNWSSSSENWNNWWWSEEEWWWNESNWSEEWWNESSWWWWWWNWWW" },
	["139" ] = { frames =  883, steps =  94, moves = "WWWWWWWWWNNWSSEEESSSENNWNNEESWWWWEESSSWWNNSSWWNESEEENNNNNWWWSSSNNNEEESSWWEESSSWWNWWSWNNNWWWWWW" },
	["139i"] = { frames =  901, steps =  96, moves = "WWSWWWWWWWSSWNNEEENNNESSWSSEENWWWWEENNNWWSSNNWWSENEEESSSSSWWWNNNSSSEEENNWWEENNNWWSWWNWSSSWWWNWWW" },
	["140" ] = { frames =  559, steps =  55, moves = "WWWWWNNWWWSSNNWWWESESSEESEENWWNNWWWEEESSWNENWWWWWWWWWWW" },
	["140i"] = { frames =  577, steps =  57, moves = "WWSWWWSSWWWNNSSWWWENENNEENEESWWSSWWWEEENNWSESWWWWWWWWNWWW" },
	["141" ] = { frames =  659, steps =  65, moves = "WWWWWSSWWNNNSSSWWWNEESSEENNNWWWWEESSWNESSWSSENNNNENWWWWSSWWWWWWWW" },
	["141i"] = { frames =  677, steps =  67, moves = "WWSWWWNNWWSSSNNNWWWSEENNEESSSWWWWEENNWSENNWNNESSSSESWWWWNNWWWWWNWWW" },
	["142" ] = { frames =  847, steps =  82, moves = "WWWWWSWENNNWSSSESWNWSESWWWWNWNESSENSEENNWWSWNEEEENNWNWWSSNNEEESSSWWWSWNENWWWWWWWWW" },
	["142i"] = { frames =  865, steps =  84, moves = "WWSWWWNWSESSWNNNENWSWNENWWWWSWSENNESNEESSWWNWSEEEESSWSWWNNSSEENENNWWWNWSESWWWWWWNWWW" },
	["143" ] = { frames =  709, steps =  74, moves = "WWNNWWWWWSSENSEESSSWWWNNWWNNNEESNWWSSSEEWWSNNNNEESSSSEEENNNWWSWWWNWWWWWWWW" },
	["143i"] = { frames =  727, steps =  76, moves = "WWSSSWWWWWNNESNEENNNWWWSSWWSSSEENSWWNNNEEWWNSSSSEENNNNEEESSSWWNWWWWWSWWWNWWW" },
	["144" ] = { frames = 1051, steps = 107, moves = "WWWWWWSSSWWWWWNNENENWSSWSSEEENEEENNWWWESSWSWWWNNENSWSSEEEEENNNNNWSESWWESSSWWWWNNENEEEESSWWNSEENNWWWWWWWWWWW" },
	["144i"] = { frames = 1069, steps = 109, moves = "WWSWWWWNNNWWWWWSSESESWNNWNNEEESEEESSWWWENNWNWWWSSESNWNNEEESESSESSWNENWWENNNWWWWSSESEEEENNWWSNEESSWWWWWWWWNWWW" },
	["145" ] = { frames = 1129, steps = 115, moves = "WWWWWNNWWWWSENWWSSSNNEESSSSEENNWNWSNNNWWSWSENESENENWWWWEESSEESSWWNNNENNWWWSWWESNESWNNWEEENEESWWWWWEESSWNNWWSSWWWWWW" },
	["145i"] = { frames = 1147, steps = 117, moves = "WWSWWWSSWWWWNESWWNNNSSEENNNNEESSWSWNSSSWWWNNESENESESWWWWWWENSEEENEENNNWWSSSESSWWWNNWSSWESEEEENWWWWWEENNWSSWWNNWWWNWWW" },
	["146" ] = { frames = 1022, steps = 104, moves = "WWWWWSSWSWWEENENNNWNWWEESSWNESSWWWNEEWWSENWSSWEEEENNWWWEEESSWSSENNWWWSEEEWWNNWSENNEWNENESESWWWWWWWWWWWWW" },
	["146i"] = { frames = 1040, steps = 106, moves = "WWSWWWNNWNWWEESESSSWSWWEENNWSENNWWWSEEWWNESWNNWEEEESSWWWEEENNWNNESSWWWNEEEWWSSWNESSEWSESENENWWWWWWWWWWNWWW" },
	["147" ] = { frames =  968, steps = 100, moves = "WWSSSWWWNNNWWSWENEESSSWWNNEESSSWWWWNNWWSENEEEESSEENWSWNNWNNWSESEENWWWESWSSWWNESENNNENWSWNNWWWWWSSWWW" },
	["147i"] = { frames =  968, steps = 100, moves = "WWNNWWWSSSWWNWSEEENNNWWSSEENNNWWWWSSWWNESEEESSWNENWNNWWSENESSESWWNEEENNEESWNWSSESWWWNWSSSWWWWWNNNWWW" },
	["148" ] = { frames = 1239, steps = 132, moves = "WWSSSWWWWENNNNNWWWSSEENNWWWWWWSSEESSNNEENNEEESSWWWEESSWWNNEENNWWWWWWSSEEEEEWWWWWNNEEEESNWWWWSSEEEESSEENNWWWWWEEENNWWWWSSSSSWWWNNNWWW" },
	["148i"] = { frames = 1221, steps = 130, moves = "WWNNWWWWESSSSSWWWNNEESSWWWWWWNNEENNSSEESSEEENNWWWEENNWWSSEESSWWWWWWNNEEEEEWWWWWSSEEEENSWWWWNNEEEENNEESSWWWWWEEESSWWWWNNNNNWWWSSWWW" },
	["149" ] = { frames =  903, steps =  94, moves = "WWWWWSSWWWSWWWWNEENSSEEEENWWWEEEENNWSENNWWWSSEENENNWWWWWSSNNWNSEENNESWSESSSEENENNWWWWWWWWWWWWW" },
	["149i"] = { frames =  921, steps =  96, moves = "WWSWWWNNWWWNWWWWSEESNNEEEESWWWEEEESSWNESSWWWNNEESESSWWWWWNNSSWSNEESSENWNENNNEESESSWWWWWWWWWWNWWW" },
}

-- Code addresses.
local TITLE_SCREEN_ADDR = 0x02bc
local SWITCH_PLAYERS_ADDR = 0x1400 -- when switching players with Select, or starting a floor in Heading Out?
local FLOOR_END_ADDR = 0x17dc -- WAY TO GO / TRY AGAIN subroutine
local MENU_ADDR = 0x2cc0 -- inside the menu subroutine, after initialization of MENU_SELECTED_ADDR, MENU_MAX_COL_ADDR, and MENU_MAX_ROW_ADDR
local MENU_RET_ADDR = 0x2df0 -- return from the menu subroutine
local COURSE_MENU_DIGIT_ADDR = 0x3cec -- code that changes the selected digit in the SELECT COURSE menu
local COURSE_MENU_RET_ADDR = 0x3d0d -- return from the SELECT COURSE menu subroutine

-- Variable addresses.
local NUM_FLOORS_ADDR = 0xc2b9
local DISPLAY_ADDR = 0xc2be
local SKILL_ADDR = 0xc2c0
local PLAYER1_POS_INDEX_ADDR = 0xc2d4
local FRAME_COUNTER_ADDR = 0xc2ec
local RNG_SEED_ADDR = 0xc377
local MENU_MAX_COL_ADDR = 0xcf11
local MENU_MAX_ROW_ADDR = 0xcf12
local MENU_SELECTED_ADDR = 0xcf19
local JOYPAD_INPUT_ADDR = 0xff8b -- the current state of joypad buttons as of the most recent poll
local JOYPAD_PRESSED_ADDR = 0xff8c -- just the joypad buttons that became newly pressed in the most recent poll

-- Constants.
local JOYPAD_BUTTON_A = 0x01
local JOYPAD_BUTTON_START = 0x08
local SKILL_EASY = 0
local SKILL_AVERAGE = 1
local SKILL_HARD = 2
local SKILL_LABELS = { [SKILL_EASY] = "easy", [SKILL_AVERAGE] = "average", [SKILL_HARD] = "hard", }
local DISPLAY_DIAGONAL = 0
local DISPLAY_BIRDSEYE = 1
local DISPLAY_LABELS = { [DISPLAY_DIAGONAL] = "diagonal", [DISPLAY_BIRDSEYE] = "birdseye", }

-- BASE_SAVESTATE is a constant (assigned in the "do" block below) that
-- represents the state of the emulator at the point at which this script is
-- run. All other savestates derive from BASE_SAVESTATE.
local BASE_SAVESTATE

-- The contents of this "do" block define a data-oriented immutable savestate
-- manipulation library. The core concept is that of a savestate object, which
-- represents the state of the emulator at a certain point. Once created, a
-- savestate object never changes. The only way to get a new savestate object
-- is to start from an existing savestate object and pass it to one of the
-- functions defined in this block (or a function defined in terms of those
-- functions, etc.). The important thing is that there is no implicit
-- dependency on the current emulator state. The idea is to remove the emulator
-- state as a hidden "global variable", mutable and available to all code, and
-- instead to make function outputs depend only on explicit inputs.
--
-- The "local" functions in the block are for internal use. They are the only
-- ones that are allowed to directly mutate the current emulator state. The
-- non-local functions exported from the block, and all other code, may change
-- the emulator state only indirectly, as an invisible side effect using the
-- safe primitives exported from this block. For example, the local function
-- run_until_event_raw effectively takes the emulator state as one of its
-- inputs and mutates the emulator state as one of its outputs; whereas the
-- non-local function run_until_event takes a savestate object as input and
-- returns a different savestate object as output.
do
	-- Hide some modules that can manipulate the emulator state and should
	-- not be used outside the savestate functions. This is not meant to be
	-- bulletproof, only to safeguard against accidental misuse.
	local joypad = joypad
	local memory = memory
	local memorysavestate = memorysavestate
	local savestate_module = savestate
	do
		local env = _ENV
		local t = {}
		setmetatable(t, {__index = function (t, key)
			if ({
				joypad = true,
				memory = true,
				memorysavestate = true,
				savestate = true,
			})[key] then
				return nil
			end
			return env[key]
		end})
		_ENV = t
	end

	-- Define SAVESTATE_DIRECTORY to save temporary savestates to disk
	-- rather than using in-memory core savestates. If you want to save the
	-- results of program execution as a movie file, you have to use actual
	-- on-disk savestates, not in-memory core savestates. This is because
	-- the latter do not store input logs. There is no function to delete
	-- the files when they are no longer used, so you will have to delete
	-- them yourself.
	local SAVESTATE_DIRECTORY = nil -- "/tmp"

	local save_emu_savestate, load_emu_savestate, delete_emu_savestate
	if SAVESTATE_DIRECTORY ~= nil then
		-- Generate temporary savestate filenames from a random
		-- basename and an incrementing counter. We're relying on
		-- math.random being somehow reasonably seeded.
		local digits = {}
		for i = 1, 16 do
			local r = math.random(16)
			digits[i] = string.sub("0123456789abcdef", r, r)
		end
		digits = table.concat(digits)
		local counter = 0
		local function generate_emu_savestate_path()
			local path = string.format("%s/tempsavestate-%s-%010d",
				SAVESTATE_DIRECTORY, digits, counter)
			counter = counter + 1
			return path
		end
		-- BizHawk does not have an API for deleting savestate files,
		-- or deleting files in general. While we cannot delete
		-- savestates that are no longer used (that have had their __gc
		-- metamethod called), we can, when creating a new savestate
		-- file, prefer to overwrite an existing, no-longer-used file,
		-- rather than create a new one. The recycle table holds a set
		-- of filenames of previously created savestate files that are
		-- no longer needed. save_emu_savestate tries to pull a name
		-- from the recycle table before creating a new file.
		local recycle = {}
		save_emu_savestate = function ()
			local path = next(recycle)
			if path ~= nil then
				recycle[path] = nil
			else
				path = generate_emu_savestate_path()
			end
			savestate_module.save(path, true)
			return path
		end
		load_emu_savestate = function (id)
			assert(savestate_module.load(id, true))
		end
		delete_emu_savestate = function (id)
			recycle[id] = true
		end
	else
		save_emu_savestate = memorysavestate.savecorestate
		load_emu_savestate = memorysavestate.loadcorestate
		delete_emu_savestate = memorysavestate.removestate
	end

	-- The metatable for all savestate objects. The important metamethod is
	-- __gc, which tells BizHawk to reclaim the resources of savestates
	-- that are no longer referenced.
	local savestate_mt = {
		__gc = function(savestate)
			delete_emu_savestate(savestate.id)
		end,
		__tostring = function(savestate)
			return string.format("savestate{%s}", savestate.id)
		end,
	}

	-- Create a new savestate object from the current state of the
	-- emulator. It is the caller's responsibility to ensure that the
	-- emulator state is frame-aligned.
	local function new_savestate()
		-- A savestate object consists of the identifier of an
		-- emulator-native savestate, and the joypad state.
		local savestate = {
			id = save_emu_savestate(),
			buttons = joypad.get(),
		}
		-- Keep the keys from joypad.get, but set all the values to false.
		for k in pairs(savestate.buttons) do
			savestate.buttons[k] = false
		end
		setmetatable(savestate, savestate_mt)
		return savestate
	end

	-- Run the emulator from its current state to the next onframestart
	-- event. The resulting aligned emulator state is safe to make or load
	-- a savestate from. See discussion starting at
	-- https://github.com/TASEmulators/BizHawk/issues/1430#issuecomment-1987446287.
	local function frame_align()
		local co = coroutine.running()
		local event_id
		event_id = event.onframestart(function ()
			event.unregisterbyid(event_id)
			assert(coroutine.resume(co))
		end)
		coroutine.yield()
	end

	-- Make the emulator state match the given savestate object. The
	-- emulator state does not need to be frame-aligned at the point when
	-- this function is called.
	local function restore_savestate(savestate)
		-- Set the current state of the joypad to what is in the
		-- savestate.
		joypad.set(savestate.buttons)

		-- The emulator state must be aligned at a frame boundary
		-- before loading a savestate. The emulator's current state is
		-- otherwise unimportant, because we throw it away.
		frame_align()
		load_emu_savestate(savestate.id)
	end

	-- Return a copy of the given savestate object.
	local function clone_savestate(savestate)
		restore_savestate(savestate)
		local new = new_savestate()
		for k, v in pairs(savestate.buttons) do
			new.buttons[k] = v
		end
		return new
	end

	-- Assume that at the point at which this script is run, it's a safe
	-- time to make a savestate (i.e., we're frame-aligned).
	BASE_SAVESTATE = new_savestate()

	-- Return a new savestate object that is a copy of a given savestate,
	-- with modified joypad buttons. The state of each button named by the
	-- keys of the buttons table is set to the corresponding value. The
	-- state of any button not represented in the buttons table is left
	-- unmodified.
	function savestate_joypad_set(savestate, buttons)
		local new = clone_savestate(savestate)
		-- Override the buttons.
		for k, v in pairs(buttons) do
			new.buttons[k] = v
		end
		return new
	end

	-- Run the emulator until the event registered by the register_event
	-- function happens, or frame_limit frames elapse, whichever happens
	-- first. If the register_event event happens first, return true; if
	-- frame_limit is exceeded first, return false. frame_limit may be an
	-- integer, or nil to mean "no limit". register_event should be a
	-- callback registration function such as event.on_bus_exec,
	-- event.onframeend, etc. The ... variable arguments are passed to
	-- register_event.
	local function run_until_event_raw(frame_limit, register_event, ...)
		if frame_limit ~= nil and frame_limit <= 0 then
			return false
		end
		local co = coroutine.running()
		-- We register two event callbacks: one for the event the
		-- caller is interested in (event_id), and one to count frames
		-- and enforce frame_limit (limit_id). We register both
		-- callbacks, then yield to let the emulator run until one of
		-- the two callbacks calls coroutine.resume. The event_id
		-- callback resumes with the value true; the limit_id callback
		-- resumes with the value false. After being resumed, we
		-- dispose of the callback that was not called. We register
		-- event_id before limit_id so that, if both callbacks happen
		-- for the same event, the caller's event takes precedence.
		local event_id = register_event(function ()
			if co ~= nil then
				assert(coroutine.resume(co, true))
			end
		end, ...)
		local frame_count = 0
		local limit_id = event.onframeend(function ()
			frame_count = frame_count + 1
			if frame_limit ~= nil and frame_count >= frame_limit then
				if co ~= nil then
					assert(coroutine.resume(co, false))
				end
			end
		end)
		local event_occurred = coroutine.yield()
		assert(event.unregisterbyid(event_id))
		assert(event.unregisterbyid(limit_id))
		-- We want only one of the two callbacks above to run
		-- coroutine.resume. At this point, one of them has done so --
		-- but just unregistering both callback IDs does not guarantee
		-- that the other will not also run coroutine.resume. That is
		-- because both callbacks may be looking for the very same
		-- event; i.e. register_event is event.onframeend and
		-- frame_limit is 1). BizHawk runs all callbacks that were
		-- registered at the time the event occurred, even if, in the
		-- course of working through the list of callbacks, a later one
		-- gets unregistered by an earlier one. We set co to nil as a
		-- signal to both callbacks that if they run, they should not
		-- call coroutine.resume.
		co = nil
		return event_occurred
	end

	-- Starting from the given savestate, return a new savestate that is
	-- the result of running until the event registered by register_event
	-- happens; or return nil if frame_limit frames elapse without the
	-- event happening. frame_limit may be nil for no limit. register_event
	-- should be a callback registration function such as
	-- event.on_bus_exec, event.onframeend, etc. The ... variable arguments
	-- are passed to register_event.
	function run_until_event(savestate, frame_limit, register_event, ...)
		restore_savestate(savestate)
		local event_occurred = run_until_event_raw(frame_limit, register_event, ...)
		if event_occurred then
			-- The event may not have occurred at a frame boundary,
			-- so align the emulator state before creating the new
			-- savestate.
			frame_align()
			return new_savestate()
		else
			return nil
		end
	end

	-- Return the result of calling f with no arguments, after making the
	-- current emulator state match the given savestate.
	local function with_savestate(savestate, f)
		restore_savestate(savestate)
		return f()
	end

	-- Return the frame count of the given savestate.
	function savestate_frame_count(savestate)
		return with_savestate(savestate, function () return emu.framecount() end)
	end

	-- Return the value of the u8 at addr in the given savestate.
	function savestate_read_u8(savestate, addr, domain)
		return with_savestate(savestate, function () return memory.read_u8(addr, domain) end)
	end
end

-- Return a new savestate one frame after the given savestate.
local function run_one_frame(savestate)
	-- It should be impossible for run_until_event to return nil
	-- (indicating that the frame_limit was exceeded) with these
	-- parameters, since event.onframeend.is the same event that
	-- run_until_event uses to count frames.
	return assert(run_until_event(savestate, 1, event.onframeend))
end

-- Return a new savestate representing the frame boundary after the address
-- addr is executed, or nil if frame_limit is reached.
local function run_until_exec(savestate, frame_limit, addr)
	return run_until_event(savestate, frame_limit, event.on_bus_exec, addr)
end

-- Return a new savestate representing the frame boundary after the address
-- addr is written, or nil if frame_limit is reached.
local function run_until_write(savestate, frame_limit, addr)
	return run_until_event(savestate, frame_limit, event.on_bus_write, addr)
end

-- Return a new savestate representing the frame boundary after the address
-- addr is written to and the written u8 value satisfies the test predicate, or
-- nil if frame_limit is reached.
local function run_until_u8_becomes_p(savestate, frame_limit, addr, test)
	repeat
		local frames = savestate_frame_count(savestate)
		savestate = run_until_event(savestate, frame_limit, event.on_bus_write, addr)
		if savestate == nil then
			return nil
		end
		local new_frames = savestate_frame_count(savestate)
		assert(new_frames >= frames)
		if frame_limit ~= nil then
			frame_limit = frame_limit - (new_frames - frames)
		end
	until test(savestate_read_u8(savestate, addr))
	return savestate
end

-- Return a new savestate representing the frame boundary after the given u8
-- value is written to the address addr, or nil if frame_limit is reached.
local function run_until_u8_becomes(savestate, frame_limit, addr, value)
	return run_until_u8_becomes_p(savestate, frame_limit, addr, function (v) return v == value end)
end

-- Return a new savestate representing the frame boundary after the address
-- addr contains a u8 value that satisfies the test predicate, or nil if
-- frame_limit is reached. This may be the same as the input savestate, if addr
-- in the input savestate already contains a satisfactory value.
local function run_until_u8_is_p(savestate, frame_limit, addr, test)
	if test(savestate_read_u8(savestate, addr)) then
		return savestate
	end
	return run_until_u8_becomes_p(savestate, frame_limit, addr, test)
end

-- Return a new savestate representing the frame boundary after the address
-- addr contains a the given u8 value, or nil if frame_limit is reached. This
-- may be the same as the input savestate, if addr in the input savestate
-- already contains the given value.
local function run_until_u8_is(savestate, frame_limit, addr, value)
	return run_until_u8_is_p(savestate, frame_limit, addr, function (v) return v == value end)
end

-- Find the earliest frame at which running an action function on a savestate
-- produces a savestate that satisfies a test predicate.
--
-- The action function takes a savestate as input and returns a savestate as
-- output. The test function takes a savestate as input and returns a Boolean
-- output (nil|false or otherwise). This function runs the action function on
-- the given savestate, then runs the test predicate on the resulting
-- savestate. If the test function's return value is not nil and not false, it
-- returns two values: the return value of action and the return value of test.
-- If the test function returns nil or false, it goes back to the original
-- savestate, advances one frame, and tries the action and test again. The
-- process continues, advancing one frame at a time, until the test predicate
-- succeeds.
--
-- It is common to call this function with a test predicate whose return value
-- is either a savestate or nil. In that case, you can regard the two return
-- values as "savestate after action performed" and "savestate after test
-- passed".
local function earliest_effective_frame(savestate, action, test)
	local action = action or function (savestate) return savestate end
	local frames = savestate_frame_count(savestate)
	while true do
		local action_performed = action(savestate)
		local test_result = test(action_performed)
		if test_result then
			return action_performed, test_result
		end
		-- No luck with this frame. Try doing the action one frame later.
		savestate = run_one_frame(savestate)
		local new_frames = savestate_frame_count(savestate)
		assert(new_frames == frames + 1, string.format("frames=%d new_frames=%d", frames, new_frames))
		frames = new_frames
	end
end

local function menu_confirm_button(savestate)
	-- Menus may be confirmed by pressing either A or Start. The button
	-- must be newly pressed in the most recent poll of the joypad; i.e.,
	-- there must have been another poll in which it was released since the
	-- last time it was pressed. There are no joypad polls between menus,
	-- so when a menu requires no directional inputs, the button used to
	-- confirm the previous menu still counts as pressed. Therefore if one
	-- of the two buttons is already pressed, we want to press the other
	-- one, to avoid having to release and re-press the first button.
	local joypad_pressed = savestate_read_u8(savestate, JOYPAD_PRESSED_ADDR)
	return ({
		[0] = "A", -- if nothing pressed, default to A
		[JOYPAD_BUTTON_START] = "A", -- if Start pressed, press A
		[JOYPAD_BUTTON_A] = "Start", -- if A pressed, press Start
		[JOYPAD_BUTTON_A | JOYPAD_BUTTON_START] = "A", -- if both pressed, default to A
	})[joypad_pressed & (JOYPAD_BUTTON_A | JOYPAD_BUTTON_START)]
end

-- Return the value x of the array t that minimizes key(x).
local function min_by_key(t, key)
	local min, min_key
	for _, x in ipairs(t) do
		local k = key(x)
		if min_key == nil or k < min_key then
			min = x
			min_key = k
		end
	end
	return min
end

local function select_menu_item(savestate, target)
	-- A menu is a 2d grid. (Most menus have just 1 column, but the SELECT
	-- FLOOR menu in Going Up? has 2 columns.) Items are indexed starting
	-- at 0, incrementing down the first column, then the second column,
	-- and so on.

	-- Seek ahead from wherever we are, until MENU_ADDR is executed and we
	-- know the menu variables are initialized. Then go back to the
	-- original savestate to start the seek forward.
	local selected, num_cols, num_rows
	do
		local menu_savestate = assert(run_until_exec(savestate, nil, MENU_ADDR))
		selected = savestate_read_u8(menu_savestate, MENU_SELECTED_ADDR)
		num_cols = savestate_read_u8(menu_savestate, MENU_MAX_COL_ADDR) + 1
		num_rows = savestate_read_u8(menu_savestate, MENU_MAX_ROW_ADDR) + 1
	end
	assert(0 <= selected and selected < num_cols * num_rows)
	assert(0 <= target and target < num_cols * num_rows)
	local selected_col = selected // num_rows
	local selected_row = selected % num_rows
	local target_col = target // num_rows
	local target_row = target % num_rows
	-- You can wrap both vertically and horizontally, either of which may
	-- be faster than not wrapping.
	local v_candidates = {
		{ button = "Down",  row_inc =  1, count = (target_row - selected_row) % num_rows },
		{ button = "Up",    row_inc = -1, count = (selected_row - target_row) % num_rows },
	}
	local h_candidates = {
		{ button = "Right", col_inc =  1, count = (target_col - selected_col) % num_cols },
		{ button = "Left",  col_inc = -1, count = (selected_col - target_col) % num_cols },
	}
	local v = min_by_key(v_candidates, function (x) return x.count end)
	local h = min_by_key(h_candidates, function (x) return x.count end)
	-- We can move the cursor once every 3 frames. We want to alternate
	-- vertical and horizontal movement as long as we have both horizontal
	-- and vertical distance to cover. After that we have to alternate with
	-- blank frames for the remaining direction. We want to start with
	-- whichever direction has more distance to cover. For example, DRDRD_D
	-- is faster than RDRD_D_D.
	local first, second
	if v.count > h.count then
		first = v
		second = h
	else
		first = h
		second = v
	end
	local function expected_update(expected, step)
		local col = expected // num_rows
		local row = expected % num_rows
		col = (col + (step.col_inc or 0)) % num_cols
		row = (row + (step.row_inc or 0)) % num_rows
		return row * num_cols + col
	end
	local function selected_becomes(expected)
		return function (savestate)
			return run_until_u8_becomes(savestate, 20, MENU_SELECTED_ADDR, expected)
		end
	end
	local expected = selected -- Keep track of the selection we expect.
	local plan = {}
	while first.count > 0 do
		first.count = first.count - 1
		expected = expected_update(expected, first)
		plan[#plan+1] = { button = first.button, test = selected_becomes(expected) }
		if second.count > 0 then
			second.count = second.count - 1
			expected = expected_update(expected, second)
			plan[#plan+1] = { button = second.button, test = selected_becomes(expected) }
		end
	end
	for _, step in ipairs(plan) do
		savestate = earliest_effective_frame(savestate, function (s)
			return savestate_joypad_set(s, {[step.button] = true})
		end, step.test)
	end
	if #plan == 0 then
		-- If #plan == 0, in other words if target == 0 (the default
		-- selection for any menu), we may actually still be in a
		-- *previous* menu that had already selected 0. Because no
		-- directional inputs are required, waiting just for the menu
		-- return MENU_RET_ADDR would find the return of the previous
		-- menu. So in this case, we wait until a few frames before
		-- *entering* MENU_ADDR, before waiting for the next
		-- MENU_RET_ADDR.
		savestate = earliest_effective_frame(savestate, nil, function (s)
			return run_until_exec(s, 2, MENU_ADDR)
		end)
	end
	local _, savestate = earliest_effective_frame(savestate, function (s)
		return savestate_joypad_set(s, {[menu_confirm_button(s)] = true})
	end, function (s)
		s = run_until_u8_is(s, 20, MENU_SELECTED_ADDR, expected)
		if s then
			s = run_until_exec(s, 20, MENU_RET_ADDR)
		end
		return s
	end)
	return savestate
end

local function cancel_menu(savestate)
	local _, savestate = earliest_effective_frame(savestate, function (s)
		return savestate_joypad_set(s, {B = true})
	end, function (s)
		local t = run_until_exec(s, 20, MENU_RET_ADDR)
		if t == nil then
			t = run_until_exec(s, 20, COURSE_MENU_RET_ADDR)
		end
		return t
	end)
	return savestate
end

local function arrays_concat(...)
	local result = {}
	for _, a in ipairs({...}) do
		table.move(a, 1, #a, #result + 1, result)
	end
	return result
end

local function select_num_floors(savestate, target)
	-- The SELECT COURSE menu in Heading Out? gives you two digits to
	-- manipulate. The variable NUM_FLOORS_ADDR tracks the state of the
	-- digits and is persistent, remaining set to the same value even
	-- across Heading Out? runs.
	local num_floors = savestate_read_u8(savestate, NUM_FLOORS_ADDR)
	local tens_digit = num_floors // 10
	local ones_digit = num_floors % 10
	local target_tens_digit = target // 10
	local target_ones_digit = target % 10
	-- For each digit, it may be faster to go up or down.
	local tens_candidates = {
		{ button = "Up",   inc =  10, count = (target_tens_digit - tens_digit) % 10 },
		{ button = "Down", inc = -10, count = (tens_digit - target_tens_digit) % 10 },
	}
	local ones_candidates = {
		{ button = "Up",   inc =   1, count = (target_ones_digit - ones_digit) % 10 },
		{ button = "Down", inc =  -1, count = (ones_digit - target_ones_digit) % 10 },
	}
	if target == 1 then
		-- In the special case of a target of 01, we also have the
		-- option of going to 00, which the game converts to 01.
		ones_candidates = arrays_concat(ones_candidates, {
			{ button = "Up",   inc =  1, count = (0 - ones_digit) % 10 },
			{ button = "Down", inc = -1, count = (ones_digit - 0) % 10 },
		})
	end
	local tens = min_by_key(tens_candidates, function (x) return x.count end)
	local ones = min_by_key(ones_candidates, function (x) return x.count end)
	local function num_floors_becomes(expected)
		return function (savestate)
			return run_until_u8_becomes(savestate, 20, NUM_FLOORS_ADDR, expected)
		end
	end
	local expected = num_floors -- Keep track of the state of num_floors we expect at each step.
	local plan = {}
	for _ = 1, tens.count do
		expected = (expected + tens.inc) // 10 * 10 + expected % 10
		plan[#plan+1] = { button = tens.button, test = num_floors_becomes(expected ) }
	end
	if ones.count > 0 then
		plan[#plan+1] = { button = "Right", test = function (s) return run_until_exec(s, 20, COURSE_MENU_DIGIT_ADDR) end }
		for _ = 1, ones.count do
			expected = expected // 10 * 10 + (expected % 10 + ones.inc) % 10
			plan[#plan+1] = { button = ones.button, test = num_floors_becomes(expected ) }
		end
	end
	for _, step in ipairs(plan) do
		savestate = earliest_effective_frame(savestate, function (s)
			return savestate_joypad_set(s, {[step.button] = true})
		end, step.test)
	end
	local _, savestate = earliest_effective_frame(savestate, function (s)
		return savestate_joypad_set(s, {[menu_confirm_button(s)] = true})
	end, function (s)
		s = run_until_u8_is(s, 20, NUM_FLOORS_ADDR, expected)
		if s then
			s = run_until_exec(s, 20, COURSE_MENU_RET_ADDR)
		end
		return s
	end)
	return savestate
end

-- Run until the byte at PLAYER1_POS_INDEX_ADDR is written to.
local function run_until_player1_moves(savestate, frame_limit)
	return run_until_write(savestate, frame_limit, PLAYER1_POS_INDEX_ADDR)
end

local function play_floor(savestate, schedule)
	savestate = run_until_exec(savestate, nil, SWITCH_PLAYERS_ADDR)
	local input_savestate = savestate
	local after_savestate = savestate
	local frame_count_start = nil
	local num_steps = 0
	for i = 1, string.len(schedule) do
		local code = string.sub(schedule, i, i)
		local button = ({
			N = "Up",
			E = "Right",
			S = "Down",
			W = "Left",
		})[code]
		assert(button, string.format("unknown code %q", code))

		input_savestate, after_savestate = earliest_effective_frame(after_savestate, function (s)
			return savestate_joypad_set(s, {[button] = true})
		end, function (s)
			return run_until_player1_moves(s, 16)
		end)

		if frame_count_start == nil then
			-- Start counting frames at the first button press
			-- (ignoring the SWITCH_PLAYERS_ADDR startup cost
			-- above).
			frame_count_start = savestate_frame_count(input_savestate)
		end
		num_steps = num_steps + 1
	end
	-- Run to the end of the frame to record the final input and get the
	-- expected frame count.
	input_savestate = run_one_frame(input_savestate)
	after_savestate = run_one_frame(after_savestate)
	-- Count frames based on after_savestate, which is when the player has
	-- fully moved. But return input_savestate, from the moment of input.
	-- This is needed to avoid empty frames at the end of the final floor.
	local num_frames = savestate_frame_count(after_savestate) - (frame_count_start or savestate_frame_count(after_savestate))
	return input_savestate, num_frames, num_steps
end

-- Starting at the "Way to go!" post-run screen, use the menus to set the
-- desired settings.
local function post_run_reconfigure(savestate, skill, num_floors, display)
	-- "Way to go!" screen.
	savestate = earliest_effective_frame(savestate, function (s)
		return savestate_joypad_set(s, {[menu_confirm_button(s)] = true})
	end, function (s)
		return run_until_exec(s, 32, MENU_ADDR)
	end)

	local cur_skill = savestate_read_u8(savestate, SKILL_ADDR)
	local cur_num_floors = savestate_read_u8(savestate, NUM_FLOORS_ADDR)
	-- The display variable uses the encoding 0 = DISPLAY_BIRDSEYE,
	-- 1 = DISPLAY_DIAGONAL which is the opposite of our convention (which
	-- uses the order in which the options appear in the menu).
	local cur_display = assert(({[0] = 1, [1] = 0})[savestate_read_u8(savestate, DISPLAY_ADDR)])

	-- There are two ways to navigate the menus: jumping back to a specific
	-- menu using the End menu, or stepping back through the menus one by
	-- one with the B Button. We try both, and keep whichever's faster.
	-- https://tasvideos.org/Forum/Posts/529194

	-- Try the End menu.
	local menu_savestate = savestate
	if cur_display ~= display or cur_num_floors ~= num_floors or cur_skill ~= skill then
		menu_savestate = select_menu_item(menu_savestate, 1) -- End
		if cur_skill ~= skill then
			menu_savestate = select_menu_item(menu_savestate, 2) -- Select skill
			menu_savestate = select_menu_item(menu_savestate, skill)
		else
			menu_savestate = select_menu_item(menu_savestate, 1) -- Select course
		end
		menu_savestate = select_num_floors(menu_savestate, num_floors)
		menu_savestate = select_menu_item(menu_savestate, display)
	end

	-- Try the B Button.
	local b_savestate = savestate
	if cur_display ~= display or cur_num_floors ~= num_floors or cur_skill ~= skill then
		b_savestate = cancel_menu(b_savestate)
		if cur_num_floors ~= num_floors or cur_skill ~= skill then
			b_savestate = cancel_menu(b_savestate)
			if cur_skill ~= skill then
				b_savestate = cancel_menu(b_savestate)
				b_savestate = select_menu_item(b_savestate, skill)
			end
			b_savestate = select_num_floors(b_savestate, num_floors)
		end
		b_savestate = select_menu_item(b_savestate, display)
	end

	-- Break ties in favor of the End menu, because that is usually faster.
	if savestate_frame_count(menu_savestate) <= savestate_frame_count(b_savestate) then
		savestate = menu_savestate
	else
		savestate = b_savestate
	end

	return savestate
end

assert(coroutine.resume(coroutine.create(function ()
	console.log("main start")
	local savestate = BASE_SAVESTATE

	-- Run until the title screen (skipping over the BIOS animation).
	savestate = run_until_exec(savestate, nil, TITLE_SCREEN_ADDR)
	-- Find the earliest frame at which we can press Start to get past the
	-- title screen.
	savestate = earliest_effective_frame(savestate, function (s)
		return savestate_joypad_set(s, {Start = true})
	end, function (s)
		return run_until_exec(s, 32, MENU_ADDR)
	end)

	savestate = select_menu_item(savestate, 1) -- Heading Out?

	local SEGMENTS = {
		-- E42x30-H34x22-H22x10-H58x8-A37x12-A02x15-A04x15-A17x9
		{ skill = SKILL_EASY,    display = DISPLAY_BIRDSEYE, seed = 42, floors = { "32i", "35i", "55i", "46", "59", "45i", "49i", "58", "56", "33", "30i", "57", "48i", "31i", "47i", "51i", "37", "41", "38", "44i", "50i", "34i", "42i", "53", "43", "54i", "40i", "36", "52i", "39i" } },
		{ skill = SKILL_HARD,    display = DISPLAY_BIRDSEYE, seed = 34, floors = { "138i", "110i", "139", "113i", "146", "148i", "111i", "144i", "143", "140", "131", "142i", "136i", "149i", "112i", "147", "121", "129i", "127i", "141", "135i", "115i" } },
		{ skill = SKILL_HARD,    display = DISPLAY_BIRDSEYE, seed = 22, floors = { "128i", "123i", "145i", "117", "114i", "125", "119", "120", "133", "137" } },
		{ skill = SKILL_HARD,    display = DISPLAY_BIRDSEYE, seed = 58, floors = { "132", "122i", "126", "124i", "134i", "130", "118", "116" } },
		{ skill = SKILL_AVERAGE, display = DISPLAY_BIRDSEYE, seed = 37, floors = { "109i", "100", "106", "104i", "98i", "65i", "63i", "93", "90", "73i", "95i", "96" } },
		{ skill = SKILL_AVERAGE, display = DISPLAY_BIRDSEYE, seed =  2, floors = { "89i", "108", "71", "94", "87i", "102", "86", "92i", "80", "103i", "60i", "91", "97", "64", "62i" } },
		{ skill = SKILL_AVERAGE, display = DISPLAY_BIRDSEYE, seed =  4, floors = { "68", "82i", "83i", "74i", "99", "70i", "76", "84i", "101", "77", "67", "90", "69", "107", "105i" } },
		{ skill = SKILL_AVERAGE, display = DISPLAY_BIRDSEYE, seed = 17, floors = { "78", "75", "61i", "79", "88", "72", "85", "81i", "66i" } },
	}

	for i, segment in ipairs(SEGMENTS) do
		-- Use the menus to set the skill, number of floors, and display.
		if i == 1 then
			savestate = select_menu_item(savestate, segment.skill)
			savestate = select_num_floors(savestate, #segment.floors)
			savestate = select_menu_item(savestate, segment.display)
		else
			-- Run until we're just about at the "Way to go!"
			-- screen. (Saves some trials in post_run_reconfigure.)
			savestate = earliest_effective_frame(savestate, function (s) return s end, function (s)
				return run_until_exec(s, 4, FLOOR_END_ADDR)
			end)
			savestate = post_run_reconfigure(savestate, segment.skill, #segment.floors, segment.display)
		end
		-- At the Start/End menu, wait for the desired RNG seed.
		savestate = earliest_effective_frame(savestate, function (s)
			return savestate_joypad_set(s, {[menu_confirm_button(s)] = true}) -- Start
		end, function (s)
			return run_until_u8_becomes(s, 10, RNG_SEED_ADDR, segment.seed)
		end)

		console.log(string.format("start segment %s%02d%s %5d",
			({[SKILL_EASY] = "E", [SKILL_AVERAGE] = "A", [SKILL_HARD] = "H"})[segment.skill],
			segment.seed,
			({[DISPLAY_DIAGONAL] = "D", [DISPLAY_BIRDSEYE] = "B"})[segment.display],
			savestate_frame_count(savestate)
		))

		-- Play the floors in the segment.
		for i, floor in ipairs(segment.floors) do
			savestate, num_frames, num_steps = play_floor(savestate, SOLUTIONS[floor].moves)
			-- Empirically determined overheads for the predicted
			-- number of frames to solve each floor.
			assert(num_steps  == SOLUTIONS[floor].steps,  string.format("steps %d != %d",  num_steps,  SOLUTIONS[floor].steps))
			assert(num_frames == SOLUTIONS[floor].frames, string.format("frames %d != %d", num_frames, SOLUTIONS[floor].frames))
		end
	end
	console.log(string.format("end %5d", savestate_frame_count(savestate)))

	movie.stop()
	console.log("main end")
end)))