Brandon
He/Him
Editor, Player (191)
Joined: 11/21/2010
Posts: 914
Location: Tennessee
Hello all, I'm planning on making my first TAS. My goal is to make one in which I complete Very Hard mode of the classic puzzle game "Panel de Pon" as fast as possible. Although I'm debating this, I am hoping to accomplish this all with a Lua bot and zero human input. I want this for a multitude of reasons:
  1. The bot could probably run on multiple versions of the same game, so long as the memory addresses are slightly modified.
  2. Improving on the speed would be a lot simpler than redoing the entire game.
  3. It could serve as an educating experience for both myself and the TAS community.
  4. AI is fun!
In its current form, it will navigate to VS, select Very Hard mode, start the match, and move to a particular location on the grid. Also, it defines a function "cursor" that can be used to tell the bot to move the cursor so that the left cursor box is at the given column (Which can be anything integer 0 to 4) and row (Which can be anything integer 0 to 10) of the 11x5 playing grid. Presumably, the next step is to parse the grid and to have the bot decide what moves to make accordingly. So, if anyone has any ideas or wants to contribute, I'm all ears. My current code can be seen below:
Language: lua

function chain() end function cursor(col, row) local mode while mode == nil or mode == 1 do mode = memory.readbyte(0x7E00B1) local coal = memory.readbyte(0x7E03A6) local roal = 15 - memory.readbyte(0x7E03AA) if coal > col then press(1, {left=true}) end if coal < col then press(1, {right=true}) end if roal <row> row then press(1, {down=true}) end if coal == col and roal == row then break end end end function draw() local grid = parse() local ascii = '' for row = 11, 1, -1 do ascii = ascii .. '|' for col = 1, 6 do ascii = ascii .. grid[col][row] .. '|' end ascii = ascii .. '\n' end gui.text(105, 0, ascii) end function parse() local array = {} for col = 0, 5 do local entry = {} for row = 0, 10 do local address = 0x7E1860 + (col * 2) - (row * 0x10) local value = memory.readbyte(address) if memory.readbyte(address + 1) ~= 0 then value = 'G' end table.insert(entry, value) end table.insert(array, entry) end return array end function press(controller, buttons) joypad.set(controller, buttons) draw() emu.frameadvance() gui.text(0, 0, tostring(buttons)) for index, value in pairs(buttons) do buttons[index] = false end joypad.set(controller, buttons) draw() emu.frameadvance() end function solve() local grid = parse() for row = 1, 11 do local blocks = {} for col = 1, 6 do local block = tostring(grid[col][row]) if blocks[block] == nil then blocks[block] = 0 end blocks[block] = blocks[block] + 1 end for index, value in pairs(blocks) do if index ~= '0' and index ~= 'G' and value > 2 then for col = 1, 5 do if tostring(grid[col][row]) == index then if tostring(grid[col + 1][row]) == index then break end cursor(col, row) press(1, {A=true}) end end for col = 6, 2, -1 do if tostring(grid[col][row]) == index then if tostring(grid[col - 1][row]) == index then break end cursor(col - 1, row) press(1, {A=true}) end end end end end for group = 1, 11 do local blocks = {} for i = 1, 7 do blocks[tostring(i)] = true end for row = 0, 2 do local missing = {} for i = 1, 7 do missing[tostring(i)] = true end for col = 1, 6 do missing[tostring(grid[col][group + row])] = nil end for index, value in pairs(missing) do blocks[index] = nil end end for index, value in pairs(blocks) do for row = 0, 1 do local start = -1 local finish = -1 for col = 1, 6 do if tostring(grid[col][group + (row * 2)]) == index then start = col break end end for col = 1, 6 do if tostring(grid[col][group + 1]) == index then finish = col break end end if start == -1 or finish == -1 then break end while start <finish> finish do cursor(start - 1, group + (row * 2)) press(1, {A=true}) start = start - 1 end end end end for row = 11, 1, -1 do for col = 1, 6 do if grid[col][row] ~= 0 and grid[col][row] ~= 'G' and row ~= 0 then if col ~= 1 and grid[col - 1][row - 1] == 0 then cursor(col - 1, row) press(1, {A=true}) end if col ~= 6 and grid[col + 1][row - 1] == 0 then cursor(col, row) press(1, {A=true}) end end end end for col = 1, 6 do if grid[col][6] ~= 0 and grid[col][6] ~= 'G' then return end end press(1, {R=true}) end while true do local menu = memory.readbyte(0x7EA138) if memory.readbyte(0x7E5564) ~= 0 and menu == 0 then press(1, {A=true}) end if menu == 1 then press(1, {A=true}) end if menu == 6 then press(1, {up=true}) press(1, {A=true}) end if menu == 10 then if memory.readbyte(0x7E9677) == 126 then press(1, {left=true}) elseif memory.readbyte(0x7E9677) == 109 then press(1, {A=true, L=true, up=true}) elseif memory.readbyte(0x7E00B1) == 1 then solve() else press(1, {start=true}) end end draw() emu.frameadvance() end
All the best, Brandon Evans
Former player
Joined: 8/31/2009
Posts: 236
I made a sloppy Yoshi's Cookie bot before, and FatRatKnight made a more polished one. If you push the script to the point where all moves are as fast as possible and everything is done in the best way mathematically, you can potentially end up with something like TAS that changes as the blocks/opponents do. I can't really help, but I really look forward to how well this goes.
Brandon
He/Him
Editor, Player (191)
Joined: 11/21/2010
Posts: 914
Location: Tennessee
Dacicus wrote:
7E1860 is the tile at the lower left corner. The address increases by 2 going right. The leftmost tile one row above is 7E1850, etc. The actual values are 0 = empty, 1 = heart, 2 = circle/octagon, 3 = triangle, 4 = star, 5 = diamond 7E1870 etc holds the oncoming row
As my computer is overloaded with encodes, I can't exactly test this, but I definitely want to post all of the data related to this in this topic. Thanks for your help!
All the best, Brandon Evans
Brandon
He/Him
Editor, Player (191)
Joined: 11/21/2010
Posts: 914
Location: Tennessee
Well, I finally did something new towards this effort. With Dacicus' information, I created an array from the playing grid, and displayed a text version of it. You can see my modified code on the first post. In addition, I have made some extensions on Dacicus' data: 6 = Upside Down Triangle 7 = Exclamation Point x = Garbage. When I say x, I mean that garbage blocks have shown up as all different numbers (Though a lot of them tend to be 6s). As such, I figure there must be some other memory address that says if a particular block is garbage or not. Dacicus, are you up to the challenge of finding what these addresses are?
All the best, Brandon Evans
Editor, Player (69)
Joined: 6/22/2005
Posts: 1050
I missed the part in the first post mentioning VS mode, so here's some clarification: 7E1860 holds the lowest left tile for Player 1, while 7E1960 holds the lowest left tile for Player 2. It appears that the byte succeeding each tile byte functions as a status indicator. A value of 0 indicates a normal tile, while a value of 4 indicates a garbage tile. For example, the lowest left tile for Player 1 would be a garbage tile if 7E1861 is 4. These are the only status values I've seen so far, but I haven't done extensive testing.
Current Projects: TAS: Wizards & Warriors III.
Brandon
He/Him
Editor, Player (191)
Joined: 11/21/2010
Posts: 914
Location: Tennessee
Another update: I created a very primitive solve function that tries to solve horizontally and vertically for 3 blocks. When it's not doing anything else, and there are less than 6 rows with blocks in them, it will raise the blocks. It takes in no consideration for the grid raising or for gravity, and it doesn't know how to make combos / chains / break garbage on purpose. That said, if you give it enough time, it can at least beat a level 1 computer in Very Hard mode, and that's certainly a good start. You can see the modified code in the first post. Thanks Dacicus for your help with labeling garbage, though note that I check for when that field is not 0, as there are more options than just 0 and 4 (Consider the case of metal garbage).
All the best, Brandon Evans
Patashu
He/Him
Joined: 10/2/2005
Posts: 4043
Once you implement the mechanics of the game within your bot, all you need to do is an alpha-beta pruned search for long combos.
My Chiptune music, made in Famitracker: http://soundcloud.com/patashu My twitch. I stream mostly shmups & rhythm games http://twitch.tv/patashu My youtube, again shmups and rhythm games and misc stuff: http://youtube.com/user/patashu
Brandon
He/Him
Editor, Player (191)
Joined: 11/21/2010
Posts: 914
Location: Tennessee
Patashu wrote:
Once you implement the mechanics of the game within your bot, all you need to do is an alpha-beta pruned search for long combos.
Please do elaborate. By game mechanics, do you mean an engine in which the bot can make moves? Because I have that as of now. I don't know anything about alpha-beta pruned searches either.
All the best, Brandon Evans
Brandon
He/Him
Editor, Player (191)
Joined: 11/21/2010
Posts: 914
Location: Tennessee
I don't want people to think I have forgotten about this run; I have not. I have started on my first real TAS, and after I finish it, I might return to this. I am a little pessimistic about making a bot to do it, though. Perhaps I can make the run manually, and then work on the bot separately to see how close to perfection I can get it. That said, would this branch even be useful? I personally think so, as I find Vs. Mode to be much more entertaining to watch. Can I get some votes on this before I attempt this?
All the best, Brandon Evans
Joined: 7/2/2007
Posts: 3960
I'd be very interested in seeing a bot play the game, especially with access to savestates. Given all the allegations about cheating computers in vs. puzzle games, it'd be nice to have the cheating AI on our side for once. :) It could also be neat to have some kind of AI competition where participants compete to make the best automatic players for various vs. games, but that's getting off-topic.
Pyrel - an open-source rewrite of the Angband roguelike game in Python.
Brandon
He/Him
Editor, Player (191)
Joined: 11/21/2010
Posts: 914
Location: Tennessee
I'm not sure why the bot would need access to save states; it's supposed to do the job correct the first time. Either way, what's your opinion on this branch regardless of the AI factor? Also, I invite you to make a topic regarding an AI competition in the Off-Topic forum; count me in if it gets enough attention.
All the best, Brandon Evans
Joined: 7/2/2007
Posts: 3960
Savestates seem like the easiest way to see the future, that's all. You could also simulate the piece generator, opponent actions, etc. to figure out what's coming up next, but that seems like a lot of work when you can just advance the emulator a few frames and take a look. As for branch, I don't have a strong feeling about it. A single match would be a pretty brief TAS, but the entire "Storyline" is probably a bit too much. Maybe have some kind of exhibition series?
Pyrel - an open-source rewrite of the Angband roguelike game in Python.
Brandon
He/Him
Editor, Player (191)
Joined: 11/21/2010
Posts: 914
Location: Tennessee
What do you mean by an exhibition series?
All the best, Brandon Evans
Joined: 7/2/2007
Posts: 3960
A series of vs. matches, that's all. I.e. more than one. A simple way to handle it would be to just use each character once.
Pyrel - an open-source rewrite of the Angband roguelike game in Python.
Brandon
He/Him
Editor, Player (191)
Joined: 11/21/2010
Posts: 914
Location: Tennessee
I'm not sure why that'd be better than Story Mode.
All the best, Brandon Evans
Joined: 5/23/2011
Posts: 1
For an optimal TAS AI, you'd need access to save states to know at what point your chain should be stopped because it has reached sufficient size to kill your opponent.
Brandon
He/Him
Editor, Player (191)
Joined: 11/21/2010
Posts: 914
Location: Tennessee
magical_pony wrote:
For an optimal TAS AI, you'd need access to save states to know at what point your chain should be stopped because it has reached sufficient size to kill your opponent.
Or you could just calculate how much is enough...
All the best, Brandon Evans