Initial public commit

This commit is contained in:
Kyle Coburn 2014-07-12 18:47:39 -07:00
commit 52232581f2
23 changed files with 7606 additions and 0 deletions

20
LICENSE Normal file
View File

@ -0,0 +1,20 @@
The MIT License (MIT)
Copyright (c) 2014 Kyle Coburn and Michael Jondahl
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.

32
Readme.md Normal file
View File

@ -0,0 +1,32 @@
PokéBot
=======
An automated computer program that speedruns Pokémon.
Pokémon Red Any%: [1:51:11](https://www.youtube.com/watch?v=M4pOlQ-mIoc) (23 June 2014)
Watch Live
==========
### [http://www.twitch.tv/thepokebot](http://www.twitch.tv/thepokebot)
PokéBot's official streaming channel on Twitch. Consider following there to find out when we're streaming, or follow the [Twitter feed](https://twitter.com/thepokebot) for announcements when we get personal best pace runs going.
Try it out
==========
Running the PokéBot on your own machine is easy. You will need a Windows environment (it runs great in VM's on Mac too). First, clone this repository (or download and unzip it) to your computer. Install the [BizHawk 1.6.1](http://down.emucr.com/v3/10194002) emulator, and procure a ROM file of Pokémon Red (you should personally own the game).
Open the ROM file with BizHawk, and Pokémon Red should start up. Then, under the 'Tools' menu, select 'Lua Console'. Click the open folder button, and navigate to the PokéBot folder you downloaded. Select 'main.lua' and press open. The bot should start running!
Seeds
=====
PokéBot comes with a built-in run recording feature that takes advantage of random number seeding to reproduce runs in their entirety. Any time the bot resets or beats the game, it will log a number to the Lua console that is the seed for the run. If you set 'CUSTOM_SEED' in main.lua to that number, the bot will reproduce your run, allowing you to share your times with others. Note that making any other modifications will prevent this from working. So if you want to make changes to the bot and share your time, be sure to fork the repo and push your changes.
Credits
=======
### Developers
Kyle Coburn: Original concept, Red routing
Michael Jondahl: Combat algorithm, Java bridge for connecting the bot to Twitch chat, Livesplit, Twitter, etc
### Special thanks
To Livesplit for providing custom component for integrating in-game time splits.
To the Pokémon speedrunning community members who inspired the idea, and shared ways to improve the bot.

249
action/battle.lua Normal file
View File

@ -0,0 +1,249 @@
local battle = {}
local textbox = require "action.textbox"
local combat = require "ai.combat"
local control = require "ai.control"
local memory = require "util.memory"
local menu = require "util.menu"
local input = require "util.input"
local utils = require "util.utils"
local inventory = require "storage.inventory"
local pokemon = require "storage.pokemon"
local function potionsForHit(potion, currHP, maxHP)
if (not potion) then
return
end
local ours, killAmount = combat.inKillRange()
if (ours) then
local potionHP
if (potion == "full_restore") then
potionHP = 999
elseif (potion == "super_potion") then
potionHP = 50
else
potionHP = 20
end
if (not currHP) then
currHP = pokemon.index(0, "hp")
maxHP = pokemon.index(0, "max_hp")
end
return math.min(currHP + potionHP, maxHP) >= killAmount - 2
end
end
battle.potionsForHit = potionsForHit
local function recover()
if (control.canRecover()) then
local currentHP = pokemon.index(0, "hp")
if (currentHP > 0) then
local maxHP = pokemon.index(0, "max_hp")
if (currentHP < maxHP) then
local first, second
if (potionIn == "full") then
first, second = "full_restore", "super_potion"
if (maxHP - currentHP > 54) then
first = "full_restore"
second = "super_potion"
else
first = "super_potion"
second = "full_restore"
end
else
if (maxHP - currentHP > 22) then
first = "super_potion"
second = "potion"
else
first = "potion"
second = "super_potion"
end
end
local potion = inventory.contains(first, second)
if (potionsForHit(potion, currentHP, maxHP)) then
inventory.use(potion, nil, true)
return true
end
end
end
end
if (memory.value("battle", "paralyzed") == 64) then
local heals = inventory.contains("paralyze_heal", "full_restore")
if (heals) then
inventory.use(heals, nil, true)
return true
end
end
end
local function openBattleMenu()
if (memory.value("battle", "text") == 1) then
input.cancel()
return false
end
local battleMenu = memory.value("battle", "menu")
local col = menu.getCol()
if (battleMenu == 106 or (battleMenu == 94 and col == 5)) then
return true
elseif (battleMenu == 94) then
local rowSelected = memory.value("menu", "row")
if (col == 9) then
if (rowSelected == 1) then
input.press("Up")
else
input.press("A")
end
else
input.press("Left")
end
else
input.press("B")
end
end
local function attack(attackIndex)
if (memory.double("battle", "opponent_hp") < 1) then
input.cancel()
elseif (openBattleMenu()) then
menu.select(attackIndex, true, false, false, false, 3)
end
end
-- Table functions
function battle.swapMove(sidx, fidx)
if (openBattleMenu()) then
local selection = memory.value("menu", "selection_mode")
local swapSelect
if (selection == sidx) then
swapSelect = fidx
else
swapSelect = sidx
end
if (menu.select(swapSelect, false, false, nil, true, 3)) then
input.press("Select")
end
end
end
function battle.isActive()
return memory.value("game", "battle") > 0
end
function battle.isTrainer()
local battleType = memory.value("game", "battle")
if (battleType == 2) then
return true
end
if (battleType == 1) then
battle.run()
else
textbox.handle()
end
end
function battle.opponent()
return pokemon.getName(memory.value("battle", "opponent_id"))
end
function battle.run()
if (memory.double("battle", "opponent_hp") < 1) then
input.cancel()
elseif (memory.value("battle", "menu") ~= 94) then
if (memory.value("menu", "text_length") == 127) then
input.press("B")
else
input.cancel()
end
elseif (textbox.handle()) then
local selected = memory.value("menu", "selection")
if (selected == 239) then
input.press("A", 2)
else
input.escape()
end
end
end
function battle.handleWild()
if (memory.value("game", "battle") ~= 1) then
return true
end
battle.run()
end
function battle.fight(move, isNumber, skipBuffs)
if (move) then
if (not isNumber) then
move = pokemon.battleMove(move)
end
attack(move)
else
move = combat.bestMove()
if (move) then
attack(move.midx)
elseif (memory.value("menu", "text_length") == 127) then
print("Faito B!")
input.press("B")
else
input.cancel()
end
end
end
function battle.swap(target)
local battleMenu = memory.value("battle", "menu")
if (utils.onPokemonSelect(battleMenu)) then
if (menu.getCol() == 0) then
menu.select(pokemon.indexOf(target), true)
else
input.press("A")
end
elseif (battleMenu == 94) then
local selected = memory.value("menu", "selection")
if (selected == 199) then
input.press("A", 2)
elseif (menu.getCol() == 9) then
input.press("Right", 0)
else
input.press("Up", 0)
end
else
input.cancel()
end
end
function movePP(name)
local midx = pokemon.battleMove(name)
if (not midx) then
return 0
end
return memory.raw(0xD02C + midx)
end
battle.pp = movePP
function battle.automate(moveName, skipBuffs)
if (not recover()) then
local state = memory.value("game", "battle")
if (state == 0) then
input.cancel()
else
if (moveName and movePP(moveName) == 0) then
moveName = nil
end
if (state == 1) then
if (control.shouldFight()) then
battle.fight(moveName, false, skipBuffs)
else
battle.run()
end
elseif (state == 2) then
battle.fight(moveName, false, skipBuffs)
end
end
end
end
return battle

105
action/shop.lua Normal file
View File

@ -0,0 +1,105 @@
local shop = {}
local textbox = require "action.textbox"
local input = require "util.input"
local memory = require "util.memory"
local menu = require "util.menu"
local player = require "util.player"
local inventory = require "storage.inventory"
function shop.transaction(options)
local item, itemMenu, menuIdx, quantityMenu
if (options.sell) then
menuIdx = 1
itemMenu = 29
quantityMenu = 158
for i,sit in ipairs(options.sell) do
local idx = inventory.indexOf(sit.name)
if (idx ~= -1) then
item = sit
item.index = idx
item.amount = inventory.count(sit.name)
break
end
end
end
if (not item and options.buy) then
menuIdx = 0
itemMenu = 123
quantityMenu = 161
for i,bit in ipairs(options.buy) do
local needed = (bit.amount or 1) - inventory.count(bit.name)
if (needed > 0) then
item = bit
item.amount = needed
break
end
end
end
if (not item) then
if (not textbox.isActive()) then
return true
end
input.press("B")
elseif (player.isFacing(options.direction or "Left")) then
if (textbox.isActive()) then
if (menu.isCurrently(32, "shop")) then
menu.select(menuIdx, true, false, "shop")
elseif (menu.getCol() == 15) then
input.press("A")
elseif (menu.isCurrently(itemMenu, "transaction")) then
if (menu.select(item.index, "accelerate", true, "transaction", true)) then
if (menu.isCurrently(quantityMenu, "shop")) then
local currAmount = memory.value("shop", "transaction_amount")
if (menu.balance(currAmount, item.amount, false, 99, true)) then
input.press("A")
end
else
input.press("A")
end
end
else
input.press("B")
end
else
input.press("A", 2)
end
else
player.interact(options.direction or "Left")
end
return false
end
function shop.vend(options)
local item
menuIdx = 0
for i,bit in ipairs(options.buy) do
local needed = (bit.amount or 1) - inventory.count(bit.name)
if (needed > 0) then
item = bit
item.buy = needed
break
end
end
if (not item) then
if (not textbox.isActive()) then
return true
end
input.press("B")
elseif (player.face(options.direction)) then
if (textbox.isActive()) then
if (memory.value("battle", "text") > 1 and memory.value("battle", "menu") ~= 95) then
menu.select(item.index, true)
else
input.press("A")
end
else
input.press("A", 2)
end
end
return false
end
return shop

77
action/textbox.lua Normal file
View File

@ -0,0 +1,77 @@
local textbox = {}
local input = require "util.input"
local memory = require "util.memory"
local menu = require "util.menu"
local utils = require "util.utils"
local alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ *():;[]ポモ-?!♂♀/.,"
local nidoName = "A"
local nidoIdx = 1
local function getLetterAt(index)
return alphabet[index]
end
local function getIndexForLetter(letter)
return alphabet:find(letter, 1, true)
end
function textbox.name(letter, randomize)
local inputting = memory.value("menu", "text_input") == 240
if (inputting) then
if (memory.value("menu", "text_length") > 0) then
input.press("Start")
return true
end
local lidx
if (letter) then
lidx = getIndexForLetter(letter)
else
lidx = nidoIdx
end
local crow = memory.value("menu", "input_row")
local drow = math.ceil(lidx / 9)
if (menu.balance(crow, drow, true, 6, true)) then
local ccol = math.floor(memory.value("menu", "column") / 2)
local dcol = math.fmod(lidx - 1, 9)
if (menu.sidle(ccol, dcol, 9, true)) then
input.press("A")
end
end
else
-- TODO cancel more when menu isn't up
if (memory.raw(0x10B7) == 3) then
input.press("A", 2)
elseif (randomize) then
input.press("A", math.random(1, 5))
else
input.cancel()
end
end
end
function textbox.getName()
return nidoName
end
function textbox.setName(index)
nidoIdx = index + 1
nidoName = getLetterAt(index)
end
function textbox.isActive()
return memory.value("game", "textbox") == 1
end
function textbox.handle()
if (not textbox.isActive()) then
return true
end
input.cancel()
end
return textbox

160
action/walk.lua Normal file
View File

@ -0,0 +1,160 @@
local walk = {}
local control = require "ai.control"
local paths = require "data.paths"
local input = require "util.input"
local memory = require "util.memory"
local player = require "util.player"
local utils = require "util.utils"
local pokemon = require "storage.pokemon"
local path, stepIdx, currentMap
local pathIdx = 0
local customIdx = 1
local customDir = 1
-- Private functions
local function setPath(index, region)
pathIdx = index
stepIdx = 2
currentMap = region
path = paths[index]
end
-- Helper functions
function dir(px, py, dx, dy)
local direction
if (py > dy) then
direction = "Up"
elseif (py < dy) then
direction = "Down"
elseif (px > dx) then
direction = "Left"
else
direction = "Right"
end
return direction
end
walk.dir = dir
function step(dx, dy)
local px, py = player.position()
if (px == dx and py == dy) then
return true
end
input.press(dir(px, py, dx, dy), 0)
end
walk.step = step
local function completeStep(region)
stepIdx = stepIdx + 1
return walk.traverse(region)
end
-- Table functions
function walk.reset()
path = nil
pathIdx = 0
customIdx = 1
customDir = 1
currentMap = nil
walk.strategy = nil
end
function walk.init()
local region = memory.value("game", "map")
local px, py = player.position()
if (region == 0 and px == 0 and py == 0) then
return false
end
for tries=1,2 do
for i,p in ipairs(paths) do
if (i > 2 and p[1] == region) then
local origin = p[2]
if (tries == 2 or (origin[1] == px and origin[2] == py)) then
setPath(i, region)
return tries == 1
end
end
end
end
end
function walk.traverse(region)
local newIndex
if (not path or currentMap ~= region) then
walk.strategy = nil
setPath(pathIdx + 1, region)
newIndex = pathIdx
customIdx = 1
customDir = 1
elseif stepIdx > #path then
return
end
local tile = path[stepIdx]
if (tile.c) then
control.set(tile)
return completeStep(region)
end
if (tile.s) then
if (walk.strategy) then
walk.strategy = nil
return completeStep(region)
end
walk.strategy = tile
elseif step(tile[1], tile[2]) then
pokemon.updateParty()
return completeStep(region)
end
return newIndex
end
function walk.canMove()
return memory.value("player", "moving") == 0 and memory.value("player", "fighting") == 0
end
-- Custom path
function walk.invertCustom(silent)
if (not silent) then
customIdx = customIdx + customDir
end
customDir = customDir * -1
end
function walk.custom(cpath, increment)
if (not cpath) then
customIdx = 1
customDir = 1
return
end
if (increment) then
customIdx = customIdx + customDir
end
local tile = cpath[customIdx]
if (not tile) then
if (customIdx < 1) then
customIdx = #cpath
else
customIdx = 1
end
return customIdx
end
local t1, t2 = tile[1], tile[2]
if (t2 == nil) then
if (player.face(t1)) then
input.press("A", 2)
end
return t1
end
if (step(t1, t2)) then
customIdx = customIdx + customDir
end
end
return walk

386
ai/combat.lua Normal file
View File

@ -0,0 +1,386 @@
local combat = {}
local movelist = require "data.movelist"
local opponents = require "data.opponents"
local memory = require "util.memory"
local utils = require "util.utils"
local damageMultiplier = { -- http://bulbapedia.bulbagarden.net/wiki/Type_chart#Generation_I
normal = {normal=1.0, fighting=1.0, flying=1.0, poison=1.0, ground=1.0, rock=0.5, bug=1.0, ghost=0.0, fire=1.0, water=1.0, grass=1.0, electric=1.0, psychic=1.0, ice=1.0, dragon=1.0, },
fighting = {normal=2.0, fighting=1.0, flying=0.5, poison=0.5, ground=1.0, rock=2.0, bug=0.5, ghost=0.0, fire=1.0, water=1.0, grass=1.0, electric=1.0, psychic=0.5, ice=2.0, dragon=1.0, },
flying = {normal=1.0, fighting=2.0, flying=1.0, poison=1.0, ground=1.0, rock=0.5, bug=2.0, ghost=1.0, fire=1.0, water=1.0, grass=2.0, electric=0.5, psychic=1.0, ice=1.0, dragon=1.0, },
poison = {normal=1.0, fighting=1.0, flying=1.0, poison=0.5, ground=0.5, rock=0.5, bug=2.0, ghost=0.5, fire=1.0, water=1.0, grass=2.0, electric=1.0, psychic=1.0, ice=1.0, dragon=1.0, },
ground = {normal=1.0, fighting=1.0, flying=0.0, poison=2.0, ground=1.0, rock=2.0, bug=0.5, ghost=1.0, fire=2.0, water=1.0, grass=0.5, electric=2.0, psychic=1.0, ice=1.0, dragon=1.0, },
rock = {normal=1.0, fighting=0.5, flying=2.0, poison=1.0, ground=0.5, rock=1.0, bug=2.0, ghost=1.0, fire=2.0, water=1.0, grass=1.0, electric=1.0, psychic=1.0, ice=2.0, dragon=1.0, },
bug = {normal=1.0, fighting=0.5, flying=0.5, poison=2.0, ground=1.0, rock=1.0, bug=1.0, ghost=0.5, fire=0.5, water=1.0, grass=2.0, electric=1.0, psychic=2.0, ice=1.0, dragon=1.0, },
ghost = {normal=0.0, fighting=1.0, flying=1.0, poison=1.0, ground=1.0, rock=1.0, bug=1.0, ghost=2.0, fire=1.0, water=1.0, grass=1.0, electric=1.0, psychic=0.0, ice=1.0, dragon=1.0, },
fire = {normal=1.0, fighting=1.0, flying=1.0, poison=1.0, ground=1.0, rock=0.5, bug=2.0, ghost=1.0, fire=0.5, water=0.5, grass=2.0, electric=1.0, psychic=1.0, ice=2.0, dragon=0.5, },
water = {normal=1.0, fighting=1.0, flying=1.0, poison=1.0, ground=2.0, rock=2.0, bug=1.0, ghost=1.0, fire=2.0, water=0.5, grass=0.5, electric=1.0, psychic=1.0, ice=1.0, dragon=0.5, },
grass = {normal=1.0, fighting=1.0, flying=0.5, poison=0.5, ground=2.0, rock=2.0, bug=0.5, ghost=1.0, fire=0.5, water=2.0, grass=0.5, electric=1.0, psychic=1.0, ice=1.0, dragon=0.5, },
electric = {normal=1.0, fighting=1.0, flying=2.0, poison=1.0, ground=0.0, rock=1.0, bug=1.0, ghost=1.0, fire=1.0, water=2.0, grass=0.5, electric=0.5, psychic=1.0, ice=1.0, dragon=0.5, },
psychic = {normal=1.0, fighting=2.0, flying=1.0, poison=2.0, ground=1.0, rock=1.0, bug=1.0, ghost=1.0, fire=1.0, water=1.0, grass=1.0, electric=1.0, psychic=0.5, ice=1.0, dragon=1.0, },
ice = {normal=1.0, fighting=1.0, flying=2.0, poison=1.0, ground=2.0, rock=1.0, bug=1.0, ghost=1.0, fire=1.0, water=0.5, grass=2.0, electric=1.0, psychic=1.0, ice=0.5, dragon=2.0, },
dragon = {normal=1.0, fighting=1.0, flying=1.0, poison=1.0, ground=1.0, rock=1.0, bug=1.0, ghost=1.0, fire=1.0, water=1.0, grass=1.0, electric=1.0, psychic=1.0, ice=1.0, dragon=2.0, },
}
local types = {}
types[0] = "normal"
types[1] = "fighting"
types[2] = "flying"
types[3] = "poison"
types[4] = "ground"
types[5] = "rock"
types[7] = "bug"
types[8] = "ghost"
types[20] = "fire"
types[21] = "water"
types[22] = "grass"
types[23] = "electric"
types[24] = "psychic"
types[25] = "ice"
types[26] = "dragon"
local savedEncounters = {}
local enablePP = false
local floor = math.floor
local function isDisabled(mid)
return mid == memory.value("battle", "disabled")
end
combat.isDisabled = isDisabled
local function calcDamage(move, attacker, defender, rng)
if (move.fixed) then
return move.fixed, move.fixed
end
if (move.power == 0 or isDisabled(move.id)) then
return 0, 0
end
if (move.power > 9000) then
local oid = defender.id
if (oid ~= 14 and oid ~= 147 and oid ~= 171 and (oidd ~= 151 or memory.value("game", "map") == 120)) then -- ???
if (memory.value("battle", "x_accuracy") == 1 and defender.speed < attacker.speed) then
return 9001, 9001
end
end
return 0, 0
end
if (move.name == "Thrash" and combat.disableThrash) then
return 0, 0
end
local attFactor, defFactor
if move.special then
attFactor, defFactor = attacker.spec, defender.spec
else
attFactor, defFactor = attacker.att, defender.def
end
local damage = floor(floor(floor(2 * attacker.level / 5 + 2) * math.max(1, attFactor) * move.power / math.max(1, defFactor)) / 50) + 2
if (move.move_type == attacker.type1 or move.move_type == attacker.type2) then
damage = floor(damage * 1.5) -- STAB
end
local dmp = damageMultiplier[move.move_type]
local typeEffect1, typeEffect2 = dmp[defender.type1], dmp[defender.type2]
if (defender.type1 == defender.type2) then
typeEffect2 = 1
end
damage = floor(damage * typeEffect1 * typeEffect2)
if (rng) then
return damage, damage
end
return floor(damage * 217 / 255), damage
end
local function getOpponentType(ty)
local t1= types[memory.value("battle", "opponent_type1")]
if ty~=0 then
t1 = types[memory.value("battle", "opponent_type2")]
if not t1 then
return memory.value("battle", "opponent_type2")
end
end
if t1 then
return t1
end
return memory.value("battle", "opponent_type1")
end
combat.getOpponentType = getOpponentType
function getOurType(ty)
local t1 = types[memory.value("battle", "our_type1")]
if (ty ~= 0) then
t1 = types[memory.value("battle", "our_type2")]
if (not t1) then
return memory.value("battle", "opponent_type2")
end
end
if t1 then
return t1
end
return memory.value("battle", "opponent_type1")
end
combat.getOurType = getOurType
local function getMoves(who)--Get the moveset of us [0] or them [1]
local moves = {}
local base
if (who == 1) then
base = 0xCFED
else
base = 0xD01C
end
for idx=0, 3 do
local val = memory.raw(base + idx)
if (val > 0) then
local moveTable = movelist.get(val)
if (who == 0) then
moveTable.pp = memory.raw(0xD02D + idx)
end
moves[idx + 1] = moveTable
end
end
return moves
end
combat.getMoves = getMoves
local function modPlayerStats(user, enemy, move)
local effect = move.effects
if (effect) then
local diff = effect.diff
local hitThem = diff < 0
local stat = effect.stat
if (hitThem) then
enemy[stat] = math.max(2, enemy[stat] + diff)
else
user[stat] = user[stat] + diff
end
end
return user, enemy
end
local function calcBestHit(attacker, defender, ours, rng)
local bestTurns, bestMinTurns = 9999, 9999
local bestDmg = -1
local ourMaxHit
local ret = nil
for idx,move in ipairs(attacker.moves) do
if (not move.pp or move.pp > 0) then
local minDmg, maxDmg = calcDamage(move, attacker, defender, rng)
if (maxDmg) then
local minTurns, maxTurns
if (maxDmg <= 0) then
minTurns, maxTurns = 9999, 9999
else
minTurns = math.ceil(defender.hp / maxDmg)
maxTurns = math.ceil(defender.hp / minDmg)
end
if (ours) then
local replaces
if (not ret or minTurns < bestMinTurns or maxTurns < bestTurns) then
replaces = true
elseif (maxTurns == bestTurns and move.name == "Thrash") then
replaces = defender.hp == memory.double("battle", "opponent_max_hp")
elseif (maxTurns == bestTurns and ret.name == "Thrash") then
replaces = defender.hp ~= memory.double("battle", "opponent_max_hp")
elseif (move.fast and not ret.fast) then
replaces = maxTurns <= bestTurns
elseif (ret.fast) then
replaces = maxTurns < bestTurns
elseif (enablePP) then
if (maxTurns < 2 or maxTurns == bestMaxTurns) then
if (ret.name == "Earthquake" and (move.name == "Ice-Beam" or move.name == "Thunderbolt")) then
replaces = true
elseif (move.pp > ret.pp) then
if (ret.name == "Horn-Drill") then
replaces = true
elseif (move.name ~= "Earthquake") then
replaces = true
end
end
end
elseif (minDmg > bestDmg) then
replaces = true
end
if (replaces) then
ret = move
bestMinTurns = minTurns
bestTurns = maxTurns
bestDmg = minDmg
ourMaxHit = maxDmg
end
elseif (maxDmg > bestDmg) then -- Opponents automatically hit max
ret = move
bestTurns = minTurns
bestDmg = maxDmg
end
end
end
end
if (ret) then
ret.damage = bestDmg
ret.maxDamage = ourMaxHit
ret.minTurns = bestMinTurns
return ret, bestTurns
end
end
local function getBestMove(ours, enemy, draw)
if (enemy.hp < 1) then
return
end
local bm, bestUs = calcBestHit(ours, enemy, true)
local jj, bestEnemy = calcBestHit(enemy, ours, false)
if (not bm) then
return
end
if draw and bm.midx then
gui.text(0, 35, ''..bm.midx.." "..bm.name)
end
return bm, bestUs, bestEnemy
end
local function activePokemon(preset)
local ours = {
id = memory.value("battle", "our_id"),
level = memory.value("battle", "our_level"),
hp = memory.double("battle", "our_hp"),
att = memory.double("battle", "our_attack"),
def = memory.double("battle", "our_defense"),
spec = memory.double("battle", "our_special"),
speed = memory.double("battle", "our_speed"),
type1 = getOurType(0),
type2 = getOurType(1),
moves = getMoves(0),
}
local enemy
if (preset) then
enemy = opponents[preset]
local toBoost = enemy.boost
if (toBoost) then
local currSpec = ours.spec
local booster = toBoost.mp
if ((currSpec < 140) == (booster > 1)) then
ours.spec = math.floor(currSpec * booster)
end
end
else
enemy = {
id = memory.value("battle", "opponent_id"),
level = memory.value("battle", "opponent_level"),
hp = memory.double("battle", "opponent_hp"),
att = memory.double("battle", "opponent_attack"),
def = memory.double("battle", "opponent_defense"),
spec = memory.double("battle", "opponent_special"),
speed = memory.double("battle", "opponent_speed"),
type1 = getOpponentType(0),
type2 = getOpponentType(1),
moves = getMoves(1),
}
end
return ours, enemy
end
combat.activePokemon = activePokemon
local function isSleeping()
return memory.raw(0xD16F) > 1
end
combat.isSleeping = isSleeping
-- Combat AI functions
function combat.factorPP(enabled)
enablePP = enabled
end
function combat.reset()
enablePP = false
end
function combat.healthFor(opponent)
local ours, enemy = activePokemon(opponent)
local enemyAttack, turnsToDie = calcBestHit(enemy, ours, false)
return enemyAttack.damage
end
function combat.inKillRange(draw)
local ours, enemy = activePokemon()
local enemyAttack, __ = calcBestHit(enemy, ours, false)
local __, turnsToKill = calcBestHit(ours, enemy, true)
if (not turnsToKill or not enemyAttack) then
return false
end
if (draw) then
gui.text(0, 21, ours.speed.." "..enemy.speed)
gui.text(0, 28, turnsToDie.." "..ours.hp.." | "..turnsToKill.." "..enemy.hp)
end
local hpReq = enemyAttack.damage
local isConfused = memory.value("battle", "confused") > 0
if (isConfused) then
hpReq = hpReq + math.floor(ours.hp * 0.2)
end
if (ours.hp < hpReq) then
local outspeed = enemyAttack.outspeed
if (outspeed and outspeed ~= true) then
outspeed = memory.value("battle", "turns") > 0
end
if (outspeed or isConfused or turnsToKill > 1 or ours.speed <= enemy.speed or isSleeping()) then
return ours, hpReq
end
end
end
local function getBattlePokemon()
local ours, enemy = activePokemon()
if (enemy.hp == 0) then
return
end
for idx=1,4 do
local move = ours.moves[idx]
if (move) then
move.midx = idx
end
end
return ours, enemy
end
function combat.nonKill()
local ours, enemy = getBattlePokemon()
if (not enemy) then
return
end
local bestDmg = -1
local ret = nil
for idx,move in ipairs(ours.moves) do
if (not move.pp or move.pp > 0) then
local __, maxDmg = calcDamage(move, ours, enemy, true)
local threshold = maxDmg * 0.95
if (threshold and threshold < enemy.hp and threshold > bestDmg) then
ret = move
bestDmg = threshold
end
end
end
return ret
end
function combat.bestMove()
local ours, enemy = getBattlePokemon()
if (enemy) then
return getBestMove(ours, enemy)
end
end
function combat.enemyAttack()
local ours, enemy = activePokemon()
if (enemy.hp == 0) then
return
end
return calcBestHit(enemy, ours, false)
end
return combat

247
ai/control.lua Normal file
View File

@ -0,0 +1,247 @@
local control = {}
local combat = require "ai.combat"
local bridge = require "util.bridge"
local memory = require "util.memory"
local menu = require "util.menu"
local paint = require "util.paint"
local player = require "util.player"
local utils = require "util.utils"
local inventory = require "storage.inventory"
local pokemon = require "storage.pokemon"
local game_controls
local shouldFight, minExp, skipHiker
local shouldCatch, attackIdx
local encounters = 0, extraEncounter
local potionIn = true
local fightEncounter, caveFights = 0, 0
local maxEncounters
local isYolo, battleYolo
local function battlePotion(enable)
potionIn = enable
end
control.battlePotion = battlePotion
local controlFunctions = {
potion = function(data)
if (data.b ~= nil) then
battlePotion(data.b)
end
battleYolo = data.yolo
end,
encounters = function(data)
if (RESET_FOR_TIME) then
maxEncounters = data.limit
extraEncounter = data.extra
end
end,
pp = function(data)
combat.factorPP(data.on)
end,
setThrash = function(data)
combat.disableThrash = data.disable
end,
disableCatch = function()
shouldCatch = nil
shouldFight = nil
end,
-- RED
viridianExp = function()
minExp = 210
shouldFight = {{name="rattata",lvl={2,3}}, {name="pidgey",lvl={2}}}
end,
viridianBackupExp = function()
minExp = 210
shouldFight = {{name="rattata",lvl={2,3}}, {name="pidgey",lvl={2,3}}}
end,
nidoranBackupExp = function()
minExp = 210
shouldFight = {{name="rattata"}, {name="pidgey"}, {name="nidoran"}, {name="nidoranf",lvl={2}}}
end,
moon1Exp = function()
if (skipHiker) then
minExp = 2704
shouldFight = {{name="zubat",lvl={9,10}}, {name="geodude"}}
oneHits = true
end
end,
moon2Exp = function()
if (skipHiker) then
minExp = 3011
shouldFight = {{name="zubat"}, {name="geodude"}, {name="paras"}}
end
end,
moon3Exp = function()
if (skipHiker) then
minExp = 3798
end
end,
catchNidoran = function()
shouldCatch = {{name="nidoran",lvl={3,4}}, {name="spearow"}}
end,
catchFlier = function()
shouldCatch = {{name="spearow",alt="pidgey",hp=15}, {name="pidgey",alt="spearow",hp=15}}
end,
catchParas = function()
shouldCatch = {{name="paras",hp=16}}
end,
catchOddish = function()
shouldCatch = {{name="oddish",alt="paras",hp=26}}
end,
}
-- Combat
local function isNewFight()
if (fightEncounter < encounters and memory.double("battle", "opponent_hp") == memory.double("battle", "opponent_max_hp")) then
fightEncounter = encounters
return true
end
end
function control.shouldFight()
if (not shouldFight) then
return false
end
local expTotal = pokemon.getExp()
if (expTotal < minExp) then
local oid = memory.value("battle", "opponent_id")
local olvl = memory.value("battle", "opponent_level")
for i,p in ipairs(shouldFight) do
if (oid == pokemon.getID(p.name) and (not p.lvl or utils.match(olvl, p.lvl))) then
if (oneHits) then
local move = combat.bestMove()
if (move and move.maxDamage * 0.925 < memory.double("battle", "opponent_hp")) then
return false
end
end
return true
end
end
end
end
function control.canCatch(partySize)
if (not partySize) then
partySize = memory.value("player", "party_size")
end
local pokeballs = inventory.count("pokeball")
local minimumCount = 4 - partySize
if (pokeballs < minimumCount) then
require("ai.strategies").reset("Not enough PokeBalls", pokeballs)
return false
end
return true
end
function control.shouldCatch(partySize)
if (maxEncounters and encounters > maxEncounters) then
local extraCount = extraEncounter and pokemon.inParty(extraEncounter)
if (not extraCount or encounters > maxEncounters + 1) then
require("ai.strategies").reset("Too many encounters", encounters)
return false
end
end
if (not shouldCatch) then
return false
end
if (not partySize) then
partySize = memory.value("player", "party_size")
end
if (partySize == 4) then
shouldCatch = nil
return false
end
if (not control.canCatch(partySize)) then
return true
end
local oid = memory.value("battle", "opponent_id")
for i,poke in ipairs(shouldCatch) do
if (oid == pokemon.getID(poke.name) and not pokemon.inParty(poke.name, poke.alt)) then
if (not poke.lvl or utils.match(memory.value("battle", "opponent_level"), poke.lvl)) then
local penultimate = poke.hp and memory.double("battle", "opponent_hp") > poke.hp
if (penultimate) then
penultimate = combat.nonKill()
end
if (penultimate) then
require("action.battle").fight(penultimate.midx, true)
else
inventory.use("pokeball", nil, true)
end
return true
end
end
end
end
-- Items
function control.canRecover()
return potionIn and (not battleYolo or not isYolo)
end
function control.set(data)
controlFunctions[data.c](data)
end
function control.setYolo(enabled)
isYolo = enabled
end
function control.setPotion(enabled)
potionIn = enabled
end
function control.encounters()
return encounters
end
function control.mtMoonExp()
print("Skipping Hiker strats")
skipHiker = true
end
function control.wildEncounter()
encounters = encounters + 1
paint.wildEncounters(encounters)
bridge.encounter()
end
function control.reset()
oneHits = false
shouldCatch = nil
shouldFight = nil
extraEncounter = nil
skipHiker = false
potionIn = true
encounters = 0
fightEncounter = 0
caveFights = 0
battleYolo = false
isYolo = false
maxEncounters = nil
end
return control

2712
ai/strategies.lua Normal file

File diff suppressed because it is too large Load Diff

1520
data/movelist.lua Normal file

File diff suppressed because it is too large Load Diff

175
data/opponents.lua Normal file
View File

@ -0,0 +1,175 @@
local opponents = {
KogaHypno = {
type1 = "psychic",
type2 = "psychic",
def = 58,
id = 129,
spec = 88,
hp = 107,
speed = 56,
level = 34,
att = 60,
moves = {
{
accuracy = 100,
name = "Confusion",
power = 50,
id = 93,
special = true,
max_pp = 25,
move_type = "psychic",
}
}
},
KogaWeezing = {
type1 = "poison",
type2 = "poison",
def = 115,
id = 143,
spec = 84,
hp = 115,
speed = 63,
level = 43,
att = 90,
moves = {
{
accuracy = 100,
name = "Self-Destruct",
power = 260,
id = 120,
special = false,
max_pp = 5,
move_type = "normal",
}
}
},
GiovanniRhyhorn = {
type1 = "ground",
type2 = "rock",
def = 97,
id = 18,
spec = 39,
hp = 134,
speed = 34,
level = 45,
att = 89,
moves = {
{
move_type = "normal",
accuracy = 100,
name = "Stomp",
power = 65,
id = 23,
special = false,
max_pp = 20,
damage = 21,
}
}
},
LoreleiDewgong = {
type1 = "water",
type2 = "ice",
def = 100,
id = 120,
spec = 116,
hp = 169,
speed = 89,
level = 54,
att = 90,
moves = {
{
accuracy = 100,
name = "Aurora-Beam",
power = 65,
id = 62,
special = true,
max_pp = 20,
move_type = "ice",
}
},
boost = {
stat = "spec",
mp = 2 / 3
}
},
LanceGyarados = {
type1 = "water",
type2 = "flying",
def = 105,
id = 22,
spec = 130,
hp = 187,
speed = 108,
level = 58,
att = 160,
moves = {
{
accuracy = 80,
name = "Hydro-Pump",
power = 120,
id = 56,
special = true,
max_pp = 5,
move_type = "water",
}
},
boost = {
stat = "spec",
mp = 1.5
}
},
BluePidgeot = {
type1 = "normal",
type2 = "flying",
def = 106,
id = 151,
spec = 100,
hp = 182,
speed = 125,
level = 61,
att = 113,
moves = {
{
accuracy = 100,
name = "Wing-Attack",
power = 35,
id = 17,
special = false,
max_pp = 35,
move_type = "flying",
}
}
},
BlueSky = {
type1 = "normal",
type2 = "flying",
def = 106,
id = 151,
spec = 100,
hp = 182,
speed = 125,
level = 61,
att = 113,
moves = {
{
accuracy = 90,
name = "Sky-Attack",
power = 140,
id = 143,
special = false,
max_pp = 5,
move_type = "flying",
}
}
},
}
return opponents

454
data/paths.lua Normal file
View File

@ -0,0 +1,454 @@
local paths = {
-- Red's room
{38, {3,6}, {5,6}, {5,1}, {7,1}},
-- Red's house
{39, {7,1}, {7,6}, {3,6}, {3,8}},
-- Into the Wild
{0, {5,6}, {10,6}, {10,1}},
-- Choose your character!
{40, {5,3}, {s="a",a="Pallet Rival"}, {5,4}, {7,4}, {s="squirtleIChooseYou"}, {5,4}, {5,6}, {s="fightBulbasaur"}, {s="split"}, {5,12}},
-- 1: RIVAL 1
-- Let's try this escape again
{0, {12,12}, {s="a",a="Pallet Town"}, {c="viridianExp"}, {c="encounters",limit=4}, {9,12}, {9,2}, {10,2}, {10,-1}},
-- First encounters
{12, {10,35}, {10,30}, {8,30}, {8,24}, {12,24}, {12,20}, {9,20}, {9,14}, {14,14}, {s="dodgePalletBoy"}, {14,2}, {11,2}, {11,-1}},
-- To the Mart
{1, {21,35}, {21,30}, {19,30}, {19,20}, {29,20}, {29,19}},
-- Viridian Mart
{42, {2,5}, {3,5}, {3,8}},
-- Backtracking
{1, {29,20}, {c="encounters",limit=5}, {29,21}, {26,21}, {26,30}, {20,30}, {20,36}},
-- Parkour
{12, {10, 0}, {10,3}, {8,3}, {8,18}, {9,18}, {9,22}, {12,22}, {12,24}, {10,24}, {10,36}},
-- To Oak's lab
{0, {10,0}, {10,7}, {9,7}, {9,12}, {12,12}, {12,11}},
-- Parcel delivery
{40, {5,11}, {5,3}, {4,3}, {4,1}, {5,1}, {s="interact",dir="Down"}, {4,1}, {4,12}},
-- Leaving home
{0, {12,12}, {c="viridianBackupExp"}, {9,12}, {9,2}, {10,2}, {10,-1}},
-- The grass again!?
{12, {10,35}, {10,30}, {8,30}, {8,24}, {12,24}, {12,20}, {9,20}, {9,14}, {14,14}, {s="dodgePalletBoy"}, {14,2}, {11,2}, {11,-1}},
-- Back to the Mart
{1, {21,35}, {21,30}, {19,30}, {19,20}, {29,20}, {29,19}},
-- Viridian Mart redux
{42, {3,7}, {3,5}, {2,5}, {s="viridianBuyPokeballs"}, {3,5}, {3,8}},
-- Sidequest
{1, {29,20}, {15,20}, {15,17}, {-1, 17}},
-- Nidoran
{33, {39, 9}, {s="a",a="Nidoran grass"}, {c="nidoranBackupExp"}, {c="encounters",limit=7,extra="spearow"}, {35, 9}, {35,12}, {33,12}, {c="catchNidoran"}, {s="catchNidoran"}, {33,12}, {s="split"}, {37,12}, {37,9}, {40,9}},
-- 2: NIDORAN
-- Out of Viridian City
{1, {0,17}, {16,17}, {16,16}, {18,16}, {18,6}, {s="dodgeViridianOldMan"}, {17, 0}, {17, -1}},
-- To the Forest
{13, {7,71}, {7,57}, {4,57}, {4,52}, {10,52}, {10,44}, {3,44}, {3,43}},
-- Forest entrance
{50, {4,7}, {s="a",a="Viridian Forest"}, {4,1}, {5,1}, {5,0}},
-- Viridian Forest
{51, {17,47}, {17,43}, {26,43}, {26,34}, {25,34}, {25,32}, {27,32}, {27,20}, {25,20}, {25,12}, {s="grabAntidote"}, {25,9}, {17,9}, {17,16}, {13,16}, {13,3}, {7,3}, {7,22}, {1,22}, {1,19}, {s="interact",dir="Up"}, {1,18}, {s="fightWeedle"}, {c="encounters",limit=22,extra="paras"}, {1,16}, {c="potion",b=false}, {s="equipForBrock",anti=true}, {1,5}, {s="equipForBrock"}, {1,-1}},
-- Forest exit
{47, {4,7}, {4,1}, {5,1}, {5,0}},
-- Road to Pewter City
{13, {3,11}, {s="a",a="Pewter City"}, {3,8}, {8,8}, {8,-1}},
-- Pewter City
{2, {18,35}, {18,22}, {19,22}, {19,13}, {10,13}, {10,18}, {16,18}, {16,17}},
-- Brock
{54, {4,13}, {s="a",a="Brock's Gym"}, {4,8}, {1,8}, {1,4}, {4,4}, {4,2}, {s="interact",dir="Up"}, {s="fightBrock"}, {s="split"}, {s="emuSpeed",percent=100}, {4,14}},
-- 3: BROCK
-- To Pewter Mart
{2, {16,18}, {c="potion",b=true}, {10,18}, {10,13}, {21,13}, {21,18}, {23,18}, {23,17}},
-- Pewter Mart
{56, {3,7}, {3,5}, {2,5}, {s="pewterMart"}, {2,6}, {3,6}, {3,8}},
-- Leaving Pewter City
{2, {23,18}, {40,18}},
-- Route 3
{14, {0,10}, {s="a",a="Route 3"}, {c="catchFlier"}, {c="pp",on=true}, {s="battleModeSet"}, {8,10}, {8,8}, {11,8}, {11,6}, {s="leer",{"caterpie",8},{"weedle",7}}, {11,4}, {12,4}, {s="potion",hp=19}, {13,4}, {s="interact",dir="Right"}, {s="shortsKid"}, {13,5}, {s="potionBeforeCocoons"}, {18,5}, {s="interact",dir="Right"}, {s="swapHornAttack"}, {18,6}, {22,6}, {22,5}, {s="potion",hp=4}, {24,5}, {s="interact",dir="Down"}, {s="fightMetapod"}, {27,5}, {27,9}, {s="catchFlierBackup"}, {37,8}, {37,5}, {49,5}, {49,10}, {57,10}, {57,8}, {59,8}, {59,-1}},
-- To the Center
{15, {9,16}, {c="pp",on=false}, {12,16}, {12,6}, {11,6}, {11,5}},
-- PP up
{68, {3,7}, {3,3}, {s="confirm",dir="Up"}, {3,8}},
-- Enter Mt. Moon
{15, {11,6}, {s="a",a="Mt. Moon"}, {18,6}, {s="split"}, {18,5}},
-- 4: ROUTE 3
-- Mt. Moon F1
{59, {14,35}, {s="startMtMoon"}, {c="catchParas"}, {14,29}, {5,29}, {5,31}, {s="interact",dir="Down"}, {5,26}, {14,26}, {14,22}, {21,22}, {21,15}, {24,15}, {24,27}, {25,27}, {25,31}, {s="interact",dir="Left"}, {25,32}, {33,32}, {33,31}, {34,31}, {s="interact",dir="Right"}, {34,7}, {30,7}, {s="evolveNidorino"}, {28,7}, {s="teachWaterGun"}, {c="moon1Exp"}, {16,7}, {16,17}, {7,17}, {7,6}, {6,6}, {s="fightHiker"}, {6,2}, {3,2}, {s="interact",dir="Left"}, {5,2}, {5,5}},
-- Mt. Moon B2
{60, {5,5}, {5,17}, {21,17}},
-- Mt. Moon B3
{61, {21,17}, {23,17}, {23,14}, {27,14}, {27,16}, {33,16}, {33,14}, {36,14}, {36,24}, {32, 24}, {32,31}, {10,31}, {10,18}, {s="evolveNidoking"}, {c="encounters",limit=nil}, {10,17}, {12,17}, {c="moon2Exp"}, {13,17}, {13,15}, {s="potion",hp=7}, {13,7}, {c="moon3Exp"}, {s="helix"}, {13,4}, {3,4}, {3,7}, {5,7}},
-- Mt. Moon escape
{60, {23,3}, {27,3}},
-- 5: MT. MOON
-- To Cerulean
{15, {24,6}, {s="reportMtMoon"}, {s="split"}, {35,6}, {35,10}, {61,10}, {61,8}, {79,8}, {79,10}, {90,10}},
-- Enter Cerulean
{3, {0,18}, {s="a",a="Cerulean"}, {14,18}, {s="dodgeCerulean"}, {19,18}, {19,17}},
-- Cerulean Center
{64, {3,7}, {3,3}, {s="confirm",dir="Up"}, {3,8}},
-- To the house
{3, {19,18}, {16,18}, {s="dodgeCeruleanLeft"}, {8,16}, {8,12}, {9,12}, {9,11}},
-- In the house
{230, {2,7}, {2,0}},
-- Outback
{3, {9,9}, {9,8}, {14,8}, {s="interact",dir="Right"}, {9,8}, {9,10}},
-- Out the house
{230, {2,1}, {2,8}},
-- Rival 2
{3, {9,12}, {s="a",a="Cerulean Rival"}, {21,12}, {21,6}, {s="rivalSandAttack"}, {21,-1}},
-- Nugget bridge
{35, {11,35}, {s="a",a="Nugget Bridge"}, {11,32}, {s="interact",dir="Up"}, {10,32}, {10,29}, {s="interact",dir="Up"}, {11,29}, {11,26}, {s="interact",dir="Up"}, {10,26}, {10,24}, {s="teachThrash"}, {10,23}, {s="interact",dir="Up"}, {11,23}, {11,21}, {s="teachThrash"}, {11,20}, {s="interact",dir="Up"}, {s="redbarMankey"}, {10,20}, {10,19}, {s="teachThrash"}, {10,15}, {s="waitToFight"}, {s="teachThrash"}, {s="split"}, {10,8}, {20,8}},
-- 6: NUGGET BRIDGE
-- To Bill's
{36, {0,8}, {9,8}, {9,7}, {11,7}, {11,9}, {14,9}, {14,6}, {15,6}, {15,4}, {17,4}, {17,7}, {18,7}, {s="interact",dir="Down"}, {20,7}, {20,8}, {22,8}, {22,6}, {35,6}, {35,4}, {36,4}, {s="interact",dir="Right"}, {36,5}, {38,5}, {38,4}, {s="interact",dir="Up"}, {45,4}, {45,3}},
-- Save Bill
{88, {2,7}, {2,5}, {5,5}, {s="confirm",dir="Right"}, {1,5}, {s="interact",dir="Up"}, {4,5}, {s="interact",dir="Up"}, {s="waitToTalk"}, {s="potionBeforeGoldeen"}, {s="item",item="escape_rope"}},
-- To Misty
{3, {19,18}, {19,20}, {30,20}, {30,19}},
-- Misty
{65, {4,13}, {s="a",a="Misty's Gym"}, {c="potion",b=false}, {4,8}, {2,8}, {2,5}, {7,5}, {7,3}, {6,3}, {5,3}, {s="waitToFight"}, {s="potionBeforeMisty"}, {5,2}, {s="waitToFight",dir="Left"}, {s="split"}, {s="tweetMisty"}, {5,3}, {7,3}, {7,5}, {5,5}, {5,14}},
-- 7: MISTY
-- Past the policeman
{3, {30,20}, {c="potion",b=true,yolo=true}, {8,20}, {8,12}, {27,12}, {27,11}},
-- Wrecked house
{62, {2,7}, {2,2}, {3,2}, {3,0}},
-- Cerulean Rocket
{3, {27,9}, {28,9}, {s="potionBeforeRocket"}, {33,9}, {33,18}, {36,18}, {36,31}, {25,31}, {25,36}},
-- Out of Cerulean
{16, {15,0}, {15,28}, {17,28}, {17,27}},
-- Underground entrance
{71, {3,7}, {3,4}, {4,4}},
-- Underground to Vermilion
{119, {5,4}, {4,4}, {s="jingleSkip"}, {2,4}, {2,41}},
-- Underground exit
{74, {4,4}, {3,8}},
-- Oddish
-- TODO Bubblebeam split
{17, {17,14}, {s="a",a="Vermilion City"}, {c="catchOddish"}, {17,15}, {s="potion",hp=9,yolo=5}, {17,19}, {s="catchOddish"}, {11,29}, {s="potion",hp=9,yolo=5}, {11,29}, {s="waitToFight",dir="Down"}, {10,29}, {10,30}, {s="potion",hp=9,yolo=5}, {10,31}, {9,31}, {9,32}, {s="potion",hp=20,yolo=18,chain=true}, {s="teach",move="bubblebeam",replace="water_gun"}, {9,36}},
-- Enter Vermilion
{5, {19,0}, {c="disableCatch"}, {19,6}, {21,6}, {21,14}, {23,14}, {23,13}},
-- Vermilion mart
{91, {3,7}, {3,5}, {2,5}, {s="vermilionMart"}, {3,5}, {3,8}},
-- To S.S. Anne
{5, {23,14}, {30,14}, {30,26}, {18,26}, {18,31}},
-- Mew
{94, {14,0}, {s="a",a="S.S. Anne"}, {14,3}},
-- First deck
{95, {27,0}, {27,1}, {26,1}, {26,7}, {2,7}, {2,6}},
-- Rival 3
{96, {2,4}, {2,11}, {3,11}, {3,12}, {s="potion",hp=20,yolo=16}, {37,12}, {37,8}, {s="rivalSandAttack"}, {37,5}, {36,5}, {36,4}},
-- Old man Cut
{101, {0,7}, {0,4}, {4,4}, {4,3}, {s="interact",dir="Up"}, {4,5}, {0,5}, {0,7}},
-- Second deck out
{96, {36,4}, {36,12}, {3,12}, {3,11}, {2,11}, {2,4}},
-- First deck out
{95, {2,6}, {2,7}, {26,7}, {26,-1}},
-- Departure
{94, {14,2}},
-- To Surge
{5, {18,29}, {18,26}, {30,26}, {30,14}, {15,14}, {15,17}, {s="potion",hp=20,yolo=5,forced="potion",chain=true}, {s="potion",hp=5,chain=true}, {s="teach",move="cut",poke="oddish",alt="paras",chain=true}, {s="skill",move="cut",done=0x0D4D}, {15,20}, {12,20}, {12,19}},
-- Trashcans
{92, {4,17}, {s="a",a="Surge's Gym"}, {4,16}, {2,16}, {2,11}, {s="trashcans"}, {4,6}, {4,3}, {5,3}, {5,2}, {s="interact",dir="Up"}, {s="fightSurge"}, {s="split"}, {4,2}, {4,13}, {5,13}, {5,18}},
-- 8: SURGE
-- To bicycle house
{5, {12,20}, {s="a",a="Bicycle Shop"}, {15,20}, {15,19}, {s="skill",move="cut",done=0x0D4D}, {15,14}, {9,14}, {9,13}},
-- Bicycle cert
{90, {2,7}, {2,5}, {0,5}, {0,1}, {2,1}, {s="confirm",dir="Right"}, {s="teach",move="dig",poke="paras",alt="squirtle",chain=true}, {s="skill",move="dig",map=90}},
-- Cerulean warp
{3, {19,18}, {19,23}, {16,23}, {16,26}, {13,26}, {13,25}},
-- Bicycle shop
{66, {2,7}, {2,5}, {4,5}, {s="dodgeBicycleGirlRight"}, {6,4}, {6,4}, {s="procureBicycle"}, {s="dodgeBicycleGirlLeft"}, {3,8}},
-- Bicycle out of Cerulean
{3, {13,26}, {s="swapBicycle"}, {s="teach",move="thunderbolt",replace="horn_attack",chain=true}, {s="bicycle"}, {19,26}, {19,27}, {s="skill",move="cut",done=0x0D4D}, {19,29}, {36,29}, {36,16}, {40,16}},
-- TPP's Bane
{20, {0,8}, {s="a",a="Route 9"}, {4,8}, {s="skill",move="cut",done=0x0C17,val=2}, {13,8}, {13,9}, {s="interact",dir="Down"}, {12,9}, {12,12}, {23,12}, {23,11}, {29,11}, {29,12}, {41,12}, {41,10}, {40,10}, {40,9}, {s="interact",dir="Up"}, {41,9}, {41,6}, {39,6}, {39,4}, {45,4}, {45,3}, {51,3}, {51,8}, {60,8}},
-- To the cave
{21, {0,8}, {3,8}, {3,10}, {13,10}, {13,15}, {14,15}, {14,26}, {3,26}, {3,18}, {8,18}, {8,17}},
-- Rock tunnel
{82, {15,3}, {s="a",a="Rock Tunnel"}, {c="potion",b=false}, {s="item",item="repel"}, {15,6}, {23,6}, {23,7}, {s="interact",dir="Down"}, {s="redbarCubone"}, {22,7}, {22,10}, {37,10}, {37,3}},
-- B1
{232, {33,25}, {33,30}, {27,30}, {s="interact",dir="Left"}, {27,31}, {14,31}, {14,29}, {s="interact",dir="Up"}, {17,29}, {17,24}, {25,24}, {25,16}, {37,16}, {37,11}, {s="item",item="repel"}, {37,3}, {27,3}},
-- B2
{82, {5,3}, {5,9}, {11,9}, {11,14}, {17,14}, {17,11}},
-- B1
{232, {23,11}, {14,11}, {14,17}, {8,17}, {8,10}, {7,10}, {s="interact",dir="Left"}, {7,11}, {5,11}, {s="item",item="repel"}, {5,3}, {3,3}},
-- Out of the Tunnel
{82, {37,17}, {32,17}, {32,23}, {37,23}, {37,28}, {28,28}, {26,24}, {23,24}, {s="interact",dir="Left"}, {23,27}, {15,27}, {15,33}},
-- To Lavender Town
{21, {8,54}, {s="a",a="Lavender Town"}, {15,54}, {15,65}, {11,65}, {11,69}, {6,69}, {6,72}},
-- Through Lavender
{4, {6,0}, {6,6}, {0,6}, {0,8}, {-1,8}},
-- Leave Lavender
{19, {59,8}, {52,8}, {52,13}, {47,13}, {s="interact",dir="Left"}, {47,14}, {42,14}, {42,7}, {40,7}, {40,6}, {29,6}, {29,7}, {23,7}, {23,12}, {14,12}, {14,4}, {13,4}, {13,3}},
-- Underground entrance
{80, {3,7}, {3,6}, {4,6}, {4,4}},
-- Underground
{121, {47,2}, {s="bicycle"}, {47,5}, {22,5}, {s="interact",dir="Left"}, {2,5}},
-- Underground exit
{77, {4,4}, {4,8}},
-- To Celadon
{18, {5,14}, {s="bicycle"}, {8,14}, {8,8}, {4,8}, {4,3}, {-1,3}},
-- Celadon
{6, {49,11}, {s="a",a="Celadon Mart"}, {14,11}, {14,14}, {10,14}, {10,13}},
-- Department store
{122, {16,7}, {c="potion",b=true,yolo=true}, {c="pp",on=true}, {16,3}, {12,3}, {12,1}},
-- F2
{123, {12,2}, {16,2}, {16,1}},
-- F3
{124, {16,2}, {12,2}, {12,1}},
-- F4: Poke Doll
{125, {12,2}, {10,2}, {10,5}, {5,5}, {s="shopPokeDoll"}, {11,5}, {11,2}, {16,2}, {16,1}},
-- F5: Buffs
{136, {16,2}, {8,2}, {8,5}, {5,5}, {s="shopBuffs"}, {9,5}, {9,2}, {12,2}, {12,1}},
-- Roof
{126, {15,3}, {12,3}, {s="shopVending"}, {6,3}, {6,4}, {s="giveWater"}, {6,4}, {7,3}, {12,3}, {s="shopExtraWater"}, {15,3}, {15,2}},
-- F5
{136, {12,2}, {16,2}, {16,1}},
-- F4
{125, {16,2}, {12,2}, {12,1}},
-- F3
{124, {12,2}, {16,2}, {16,1}},
-- F2: TM and repel
{123, {16,2}, {8,2}, {8,5}, {6,5}, {s="shopTM07"}, {5,5}, {s="shopRepels"}, {9,5}, {9,2}, {12,2}, {12,1}},
-- Exit department store
{122, {12, 2}, {12,6}, {16,6}, {16,8}},
-- Leave Celadon
{6, {10,14}, {s="bicycle"}, {10,15}, {2,15}, {2,18}, {-1,18}},
-- Cut out of Celadon
{27, {39,10}, {34,10}, {s="teach",move="horn_drill",replace="bubblebeam",full=true,chain=true}, {s="skill",move="cut",dir="Up",done=0x0D4D}, {34,6}, {27,6}, {27,4}, {23,4}},
-- Old man's hall
{186, {7,2}, {-1,2}},
-- To the Fly house
{27, {17,4}, {s="a",a="HM02 Fly"}, {10,4}, {10,6}, {7,6}, {7,5}},
-- Fly house
{188, {2,7}, {2,4}, {s="interact",dir="Up"}, {2,5}, {s="split"}, {2,8}},
-- 9: FLY
-- Fly to Lavender
{27, {7,6}, {s="swapRepels"}, {s="teach",move="fly",poke="spearow",alt="pidgey",chain=true}, {s="teach",move="horn_drill",replace="bubblebeam",chain=true}, {s="item",item="super_repel",chain=true}, {s="potion",hp=6,chain=true}, {s="teach",move="rock_slide",replace="poison_sting",chain=true}, {s="fly",dest="lavender",map=4}},
-- To the tower
{4, {3,6}, {s="a",a="Pokemon Tower"}, {14,6}, {14,5}},
-- Pokemon Tower
{142, {10,17}, {10,10}, {18,10}, {18,9}},
-- F2: Rival
{143, {18,9}, {c="setThrash",disable=true}, {18,7}, {16,7}, {16,5}, {15,5}, {s="lavenderRival"}, {5,5}, {5,8}, {3,8}, {3,9}},
-- F3
{144, {3,9}, {3,10}, {6,10}, {6,13}, {8,13}, {8,6}, {17,6}, {17,9}, {18,9}},
-- F4
{145, {18,9}, {s="allowDeath",on=true}, {c="potion",b=false}, {18,7}, {16,7}, {s="interact",dir="Left"}, {s="digFight"}, {16,9}, {c="potion",b=true,yolo=true}, {14,9}, {14,10}, {13,10}, {s="interact",dir="Left"}, {14,10}, {14,8}, {11,8}, {11,9}, {10,9}, {10,12}, {7,12}, {7,11}, {4,11}, {4,10}, {3,10}, {3,9}},
-- F5
{146, {3,9}, {4,9}, {4,11}, {s="interact",dir="Down"}, {4,6}, {13,6}, {13,9}, {9,9}, {9,12}, {14,12}, {14,10}, {18,10}, {s="allowDeath",on=false}, {18,9}},
-- F6
{147, {18,9}, {18,7}, {15,7}, {15,3}, {11,3}, {11,5}, {10,5}, {s="interact",dir="Left"}, {10,6}, {6,6}, {6,7}, {s="interact",dir="Down"}, {6,14}, {10,14}, {10,16}, {s="pokeDoll"}, {9,16}},
-- F7: Top
{148, {9,16}, {10,16}, {10,9}, {s="fightXAccuracy"}, {c="setThrash",disable=false}, {10,7}, {s="thunderboltFirst"}, {10,4}, {s="interact",dir="Up"}},
-- Old man's house
{149, {3,7}, {3,6}, {2,6}, {2,1}, {s="interact",dir="Right"}, {2,8}},
-- 10: POKéFLUTE
-- Lavender -> Celadon
{4, {7,10}, {s="split"}, {s="fly",dest="celadon",map=6}},
-- To Celadon Center
{6, {41,10}, {41,9}},
-- Celadon Center
{133, {3,7}, {3,3}, {s="confirm",dir="Up"}, {3,8}},
-- Leave Celadon
{6, {41,10}, {s="a",a="Snorlax"}, {s="item",item="super_repel",chain=true}, {s="bicycle"}, {41,11}, {14,11}, {14,14}, {2,14}, {2,18}, {-1,18}},
-- トトロだ!
{27, {39,10}, {27,10}, {s="playPokeflute"}, {23,10}},
-- Snorlax pass
{186, {7,8}, {-1,8}},
-- Bicycle road
{27, {17,10}, {s="a",a="Bicycle Road"}, {12,10}, {12,13}, {11,13}, {11,18}},
-- Forced down inputs
{28, {11,0}, {11,5}, {15,5}, {15,12}, {s="drivebyRareCandy"}, {15,14}, {18,14}, {18,122}, {13,122}, {13,143}},
-- Cycling road exit
{29, {13,0}, {13,8}, {34,8}},
-- Exit building
{190, {0,4}, {8,4}},
-- Enter Safari City
{29, {40,8}, {s="item",item="super_repel",chain=true}, {s="teach",move="ice_beam",replace="rock_slide",chain=true}, {s="bicycle"}, {50,8}},
-- Safari City
{7, {0,16}, {s="a",a="Safari Zone"}, {3,16}, {3,20}, {23,20}, {23,14}, {29,14}, {29,15}, {35,15}, {35,8}, {37,8}, {37,2}, {22,2}, {22,4}, {18,4}, {18,3}},
-- Safari entrance
{156, {3,5}, {3,2}, {4,2}, {s="confirm",dir="Right"}},
-- Safari 1
{220, {15,25}, {s="bicycle"}, {15,16}, {28,16}, {28,11}, {30,11}},
-- Safari 2
{217, {0,23}, {4,23}, {4,24}, {20,24}, {20,20}, {s="safariCarbos"}, {12,20}, {12,22}, {11,22}, {10,22}, {s="item",item="super_repel",chain=true}, {s="item",item="carbos",poke="nidoking",close=true}, {9,22}, {9,8}, {12,8}, {12,6}, {17,6}, {17,8}, {20,8}, {s="centerSkipFullRestore"}, {20,3}, {7,3}, {7,5}, {-1,5}},
-- Safari 3
{218, {39,31}, {22,31}, {22,22}, {16,22}, {16,28}, {13,28}, {13,9}, {28,9}, {28,3}, {3,3}, {3,36}},
-- Safari 4
{219, {21,0}, {21,5}, {19,5}, {19,6}, {s="interact",dir="Down"}, {19,5}, {7,5}, {7,6}, {3,6}, {3,3}},
-- Warden
{222, {2,7}, {2,6}, {3,6}, {3,4}, {s="interact",dir="Up"}, {3,8}},
-- Safari Warp
{219, {3,4}, {s="skill",move="dig",map=219}},
-- Celadon again
{6, {41,10}, {s="bicycle"}, {41,11}, {50,11}},
-- To Saffron
{18, {0,3}, {s="a",a="Saffron City"}, {4,3}, {4,9}, {10,9}, {10,10}, {12,10}},
-- Thirsty guard
{76, {0,4}, {6,4}},
-- Saffron entry
{18, {18,10}, {s="bicycle"}, {20,10}},
-- Saffron City
{10, {0,18}, {3,18}, {3,22}, {18,22}, {18,21}},
-- Silph Co
{181, {10,17}, {s="a",a="Silph Co."}, {10,9}, {8,9}, {8,1}, {20,1}, {20,0}},
-- Elivator
{236, {1,3}, {1,2}, {3,2}, {3,1}, {s="silphElevator"}, {2,1}, {2,4}},
-- F10
{234, {12,1}, {12,3}, {4,3}, {4,9}, {s="fightSilphMachoke"}, {6,9}, {6,11}, {s="silphCarbos"}, {6,16}, {3,16}, {3,14}, {s="interact",dir="Right"}, {3,15}, {1,15}, {s="item",item="carbos",poke="nidoking",full=true}, {1,12}, {s="interact",dir="Right"}, {1,16}, {3,16}, {s="teach",move="surf",poke="squirtle",replace="tail_whip",chain=true}, {s="teach",move="earthquake",replace="thrash",chain=true}, {s="item",item="carbos",poke="nidoking",close=true}, {6,16}, {6,9}, {4,9}, {4,1}, {8,1}, {8,0}},
-- F9
{233, {14,1}, {14,3}, {24,3}, {24,16}, {17,16}, {17,15}},
-- Warped
{210, {9,15}, {9,16}, {20,16}, {s="interact",dir="Right"}, {9,16}, {9,15}},
-- Warp back
{233, {17,15}, {17,14}, {17,15}},
-- First card
{210, {9,15}, {9,13}, {8,13}, {s="interact",dir="Left"}, {6,13}, {6,16}, {3,16}, {3,15}},
-- Warp down
{208, {3,15}, {3,14}, {18,14}, {18,9}, {s="interact",dir="Left"}, {14,9}, {14,11}, {11,11}},
-- Rival 5
{212, {5,3}, {s="a",a="Silph Rival"}, {4,3}, {4,2}, {3,2}, {c="potion",b=false}, {s="silphRival"}, {3,7}, {c="potion",b=true,yolo=true}, {5,7}},
-- Giovanni
{235, {3,2}, {s="a",a="Silph Giovanni"}, {3,11}, {2,11}, {2,16}, {s="interact",dir="Right"}, {2,15}, {5,15}, {s="potion",hp=16,yolo=12}, {6,15}, {6,14}, {s="interact",dir="Up"}, {6,13}, {s="fightXAccuracy"}, {s="fightSilphGiovanni"}, {s="split"}, {s="waitToPause"}, {s="skill",move="dig",map=235}},
-- 11: SILPH CO.
-- Fly to Fuschia
{6, {41,10}, {s="fly",dest="fuchsia",map=7}},
-- To Koga
{7, {19,28}, {s="a",a="Koga's Gym"}, {5,28}, {5,27}},
-- Koga
-- TODO save turn frames?
{157, {4,17}, {9,17}, {9,9}, {7,9}, {s="interact",dir="Up"}, {9,9}, {9,1}, {1,1}, {1,2}, {s="potion",hp="KogaHypno",yolo=18,chain=true}, {s="earthquakeElixer",min=2,close=true}, {1,3}, {2,3}, {2,5}, {1,5}, {c="potion",b=false}, {1,7}, {s="fightHypno"}, {1,9}, {2,9}, {s="earthquakeElixer",min=4}, {4,9}, {s="interact",dir="Down"}, {s="fightKoga"}, {s="split"}, {1,9}, {1,5}, {2,5}, {2,3}, {1,3}, {1,1}, {9,1}, {9,16}, {5,16}, {5,18}},
-- 12: KOGA
-- To the Warden
{7, {5,28}, {s="bicycle"}, {6,28}, {6,30}, {24,30}, {30,30}, {30,28}, {27,28}, {27,27}},
-- HM04 Strength
{155, {4,7}, {4,6}, {2,6}, {2,4}, {s="interact",dir="Up"}, {4,4}, {4,8}},
-- Fly home
{7, {27,28}, {s="fly",dest="pallet",map=0}},
-- Pallet to Cinnabar
{0, {5,6}, {s="item",item="super_repel",chain=true}, {s="item",item="rare_candy",amount=3,poke="nidoking",chain=true}, {s="bicycle"}, {s="allowDeath",on=false}, {3,6}, {s="dodgeGirl"}, {3,17}, {s="skill",move="surf",dir="Right",x=4}, {4,18}},
-- To Cinnabar
{32, {4,0}, {4,14}, {3,14}, {3,90}},
-- Enter Cinnabar Mansion
{8, {3,0}, {s="a",a="Cinnabar Mansion"}, {3,4}, {6,4}, {6,3}},
-- F1
{165, {5,27}, {5,10}},
-- F2
{214, {5,11}, {10,11}, {10,5}, {6,5}, {6,1}},
-- F3
{215, {6,2}, {11,2}, {11,6}, {10,6}, {s="confirm",dir="Up"}, {14,6}, {14,11}, {16,11}, {16,14}},
-- F1 drop
{165, {16,14}, {16,15}, {13,15}, {13,20}, {s="cinnabarCarbos"}, {21,23}},
-- B1
{216, {23,22}, {23,15}, {21,15}, {s="item",item="super_repel",chain=true}, {s="item",item="carbos",poke="nidoking",close=true}, {17,15}, {17,19}, {18,19}, {18,23}, {17,23}, {17,26}, {18,26}, {s="confirm",dir="Up"}, {14,26}, {14,22}, {12,22}, {12,15}, {24,15}, {24,18}, {26,18}, {26,6}, {24,6}, {24,4}, {20,4}, {s="confirm",dir="Up"}, {24,4}, {24,6}, {12,6}, {12,2}, {11,2}, {s="interact",dir="Left"}, {12,2}, {12,7}, {4,7}, {4,9}, {2,9}, {s="interact",dir="Left"}, {5,9}, {5,10}, {s="teach",move="strength",poke="squirtle",replace="tackle",chain=true}, {s="item",item="rare_candy",amount=2,poke="nidoking",close=true}, {5,12}, {s="interact",dir="Down"}, {5,12}, {s="skill",move="dig",map=216}},
-- Celadon once again
{6, {41,10}, {s="bicycle"}, {41,13}, {36,13}, {36,23}, {25,23}, {25,30}, {35,30}, {35,31}, {s="skill",move="cut",dir="Down",done=0x0D4D}, {35,34}, {5,34}, {5,29}, {12,29}, {12,27}},
-- Erika
{134, {4,17}, {s="a",a="Erika's Gym"}, {4,16}, {1,16}, {1,9}, {0,9}, {0,4}, {1,4}, {s="skill",move="cut",done=0x0D4D}, {4,4}, {s="interact",dir="Up"}, {s="fightErika"}, {s="split"}, {4,5}, {5,5}, {5,6}, {s="skill",move="cut",dir="Down",done=0x0D4D}, {5,18}},
-- 13: ERIKA
-- Fly to Cinnabar
{6, {12,28}, {s="fly",dest="cinnabar",map=8}},
-- Cinnabar
{8, {11,12}, {s="earthquakeElixer",min=4,chain=true}, {s="bicycle"}, {18,12}, {18,3}},
-- Cinnabar Gym
{166, {16,17}, {s="a",a="Blaine's Gym"}, {16,14}, {18,14}, {18,10}, {15,10}, {15,8}, {s="confirm",dir="Up"}, {16,8}, {16,7}, {18,7}, {18,1}, {12,1}, {12,2}, {10,2}, {s="confirm",dir="Up",type="B"}, {12,2}, {12,7}, {10,7}, {10,8}, {9,8}, {s="confirm",dir="Up",type="B"}, {9,11}, {12,11}, {12,13}, {10,13}, {10,14}, {9,14}, {s="confirm",dir="Up",type="B"}, {9,16}, {1,16}, {1,14}, {s="confirm",dir="Up"}, {2,14}, {2,13}, {4,13}, {4,9}, {1,9}, {1,8}, {s="confirm",dir="Up",type="B"}, {2,8}, {2,7}, {4,7}, {4,5}, {3,5}, {3,4}, {c="potion",b=false}, {s="waitToFight",dir="Up"}, {s="split"}, {s="waitToReceive"}, {s="skill",move="dig",map=166}},
-- 14: BLAINE
-- Celadon too many times
{6, {41,10}, {c="potion",b=true,yolo=true}, {s="bicycle"}, {41,11}, {50,11}},
-- Exit Celadon
{18, {0,3}, {4,3}, {4,9}, {10,9}, {10,10}, {12,10}},
-- Saffron gate
{76, {0,4}, {s="a",a="Saffron City"}, {6,4}},
-- Saffron edge
{18, {18,10}, {s="earthquakeElixer",min=4,chain=true}, {s="bicycle"}, {20,10}},
-- Saffron again
{10, {0,18}, {3,18}, {3,6}, {31,6}, {31,4}, {34,4}, {34,3}},
-- Sabrina
{178, {8,17}, {s="a",a="Sabrina's Gym"}, {8,16}, {11,16}, {11,15}, {16,17}, {16,15}, {15,15}, {18,3}, {18,5}, {15,5}, {1,5}, {11,11}, {11,8}, {10,8}, {s="waitToFight",dir="Left"}, {s="split"}, {11,8}, {11,11}, {s="earthquakeElixer",min=4,chain=true}, {s="skill",move="dig",map=178}},
-- 15: SABRINA
-- Celadon
{6, {41,10}, {s="fly",dest="viridian",map=1}},
-- Viridian again
{1, {23,26}, {s="bicycle"}, {19,26}, {19,4}, {27,4}, {27,3}, {34,3}, {34,8}, {32,8}, {32,7}},
-- Giovanni Gym
{45, {16,17}, {c="potion",b=false}, {s="a",a="Giovanni's Gym"}, {16,16}, {14,16}, {14,9}, {13,9}, {13,7}, {15,7}, {15,4}, {12,4}, {12,5}, {10,5}, {s="a",a="Machoke"}, {10,4}, {s="fightGiovanniMachoke"}, {10,5}, {s="a",a="Giovanni's Gym"}, {13,5}, {13,4}, {15,4}, {15,7}, {13,7}, {13,11}, {14,11}, {14,16}, {16,16}, {16,18}},
-- Reset Gym
{1, {32,8}, {32,7}},
-- Giovanni
{45, {16,17}, {c="potion",b=false}, {16,16}, {14,16}, {14,9}, {13,9}, {13,7}, {15,7}, {15,4}, {12,4}, {12,5}, {10,5}, {10,2}, {7,2}, {7,4}, {2,4}, {s="checkGiovanni"}, {2,2}, {s="interact",dir="Up"}, {s="fightGiovanni"}, {s="split"}, {2,4}, {7,4}, {7,2}, {10,2}, {10,5}, {12,5}, {12,4}, {15,4}, {15,7}, {13,7}, {13,11}, {14,11}, {14,16}, {16,16}, {16,18}},
-- 16: GIOVANNI
-- Leave Viridian
{1, {32,8}, {s="bicycle"}, {32,12}, {17,12}, {17,16}, {16,16}, {16,17}, {-1,17}},
-- To Pokemon League
{33, {39,9}, {s="a",a="Viridian Rival"}, {35,9}, {35,12}, {31,12}, {31,5}, {29,5}, {s="viridianRival"}, {16,5}, {16,12}, {5,12}, {5,10}, {11,10}, {11,6}, {8,6}, {8,5}},
-- Pokemon League 1
{193, {4,7}, {4,0}},
-- PL 2
{34, {7,139}, {s="ether",max=true,chain=true}, {s="bicycle"}, {7,132}, {14,132}, {14,124}, {9,124}, {9,116}, {10,116}, {10,104}, {s="skill",move="surf",y=103}, {10,92}, {7,92}, {7,90}, {s="pickMaxEther"}, {7,72}, {8,72}, {8,71}, {s="item",item="super_repel",chain=true}, {s="bicycle"}, {8,66}, {10,66}, {10,57}, {12,57}, {12,48}, {6,48}, {6,32}, {4,32}, {4,31}},
-- Victory Road
{108, {8,17}, {s="a",a="Victory Road"}, {s="tweetVictoryRoad"}, {8,16}, {4,16}, {4,14}, {5,14}, {s="skill",move="strength"}, {5,15}, {4,15}, {4,16}, {7,16}, {s="push",dir="Right",x=0x0255,y=0x0254}, {7,17}, {9,17}, {9,15}, {8,15}, {8,14}, {15,14}, {15,15}, {16,15}, {16,14}, {s="push",dir="Up",x=0x0255,y=0x0254}, {14,14}, {14,12}, {16,12}, {16,11}, {17,11}, {17,12}, {14,12}, {14,14}, {8,14}, {8,16}, {5,16}, {5,12}, {11,12}, {11,6}, {7,6}, {7,8}, {3,8}, {3,5}, {2,5}, {2,1}, {1,1}},
-- F2
{194, {0,8}, {0,9}, {3,9}, {3,13}, {5,13}, {5,14}, {s="item",item="super_repel",chain=true}, {s="skill",move="strength"}, {4,14}, {4,13}, {3,13}, {3,15}, {4,15}, {4,16}, {3,16}, {s="push",dir="Left",x=0x02B5,y=0x02B4}, {3,11}, {5,11}, {5,8}, {14,8}, {14,14}, {21,14}, {21,16}, {28,16}, {28,11}, {23,11}, {23,7}},
-- F3
{198, {23,7}, {23,6}, {22,6}, {22,4}, {s="skill",move="strength"}, {22,2}, {23,2}, {23,1}, {7,1}, {7,0}, {6,0}, {6,1}, {7,1}, {7,2}, {3,2}, {3,1}, {2,1}, {2,4}, {1,4}, {1,5}, {2,5}, {2,4}, {4,4}, {4,2}, {7,2}, {7,1}, {20,1}, {20,6}, {17,6}, {17,4}, {9,4}, {9,10}, {5,10}, {5,8}, {1,8}, {1,15}, {11,15}, {11,16}, {20,16}, {20,15}, {23,15}},
-- F2
{194, {22,16}, {s="healBeforeLorelei"}, {s="item",item="super_repel",chain=true}, {s="skill",move="strength"}, {s="bicycle"}, {22,17}, {24,17}, {24,16}, {11,16}, {s="push",dir="Left",x=0x02D5,y=0x02D4}, {21,16}, {21,14}, {25,14}},
-- F3
{198, {27,15}, {27,8}, {26,8}},
-- F2 Exit
{194, {27,7}, {30,7}},
-- Victory end
{34, {14,32}, {18,32}, {18,20}, {14,20}, {14,10}, {13,10}, {13,6}, {10,6}, {10,-1}},
-- Elite Four entrance
{9, {10,17}, {s="a",a="Elite Four"}, {10,5}},
-- Last Center
{174, {7,11}, {15,9}, {15,8}, {s="depositPokemon"}, {7,8}, {7,7}, {s="centerSkip"}, {4,7}, {3,7}, {3,2}, {8,2}, {8,0}},
-- 17: LORELEI
{245, {4,5}, {s="a",a="Lorelei"}, {c="potion",b=false}, {4,2}, {s="interact",dir="Right"}, {s="lorelei"}, {s="split"}, {4,0}},
{246, {4,5}, {s="a",a="Bruno"}, {s="item",item="elixer",poke="nidoking"}, {4,2}, {s="interact",dir="Right"}, {s="bruno"}, {s="split"}, {4,0}},
{247, {4,5}, {s="a",a="Agatha"}, {s="potion",hp=113,full=true}, {4,2}, {s="interact",dir="Right"}, {s="agatha"}, {s="split"}, {4,1}, {s="prepareForLance"}, {s="ether"}, {4,0}},
{113, {6,11}, {s="a",a="Lance"}, {6,2}, {s="lance"}, {s="waitToFight"}, {s="split"}, {5,2}, {5,1}, {s="prepareForBlue"}, {5,-1}},
{120, {4,3}, {s="a",a="Blue"}, {s="blue"}, {3,0}},
{118, {4,2}, {s="champion"}}
}
return paths

198
main.lua Normal file
View File

@ -0,0 +1,198 @@
-- Customization settings
GAME_NAME = "red" -- Only currently supported option
RESET_FOR_TIME = true -- Set to false if you just want to see the bot finish a run
local CUSTOM_SEED = nil -- Set to a known seed to replay it, or leave nil for random ones
local PAINT_ON = true -- Displays contextual information while the bot runs
-- Start code (hard hats on)
local START_WAIT = 99
local VERSION = "1.0"
local battle = require "action.battle"
local textbox = require "action.textbox"
local walk = require "action.walk"
local combat = require "ai.combat"
local control = require "ai.control"
local strategies = require "ai.strategies"
local bridge = require "util.bridge"
local input = require "util.input"
local memory = require "util.memory"
local menu = require "util.menu"
local paint = require "util.paint"
local utils = require "util.utils"
local settings = require "util.settings"
local pokemon = require "storage.pokemon"
local YELLOW = GAME_NAME == "yellow"
local hasAlreadyStartedPlaying = false
local inBattle, oldSecs
local running = true
local previousPartySize = 0
local lastHP
local criticaled = false
local function startNewAdventure()
local startMenu, withBattleStyle
if (YELLOW) then
startMenu = memory.raw(0x0F95) == 0
withBattleStyle = "battle_style"
else
startMenu = memory.value("player", "name") ~= 0
end
if (startMenu and menu.getCol() ~= 0) then
if (settings.set("text_speed", "battle_animation", withBattleStyle)) then
menu.select(0)
end
elseif (math.random(0, START_WAIT) == 0) then
input.press("Start")
end
end
local function choosePlayerNames()
local name
if (memory.value("player", "name2") == 80) then
name = "W"
else
name = "B"
end
textbox.name(name, true)
end
local function pollForResponse()
local response = bridge.process()
if (response) then
bridge.polling = false
textbox.setName(tonumber(response))
end
end
local function resetAll()
strategies.softReset()
combat.reset()
control.reset()
walk.reset()
paint.reset()
bridge.reset()
oldSecs = 0
running = false
previousPartySize = 0
-- client.speedmode = 200
if (CUSTOM_SEED) then
strategies.seed = CUSTOM_SEED
print("RUNNING WITH A FIXED SEED ("..strategies.seed.."), every run will play out identically!")
else
strategies.seed = os.time()
end
math.randomseed(strategies.seed)
end
-- Execute
print("Welcome to PokeBot "..GAME_NAME.." version "..VERSION)
local productionMode = not walk.init() and false
if (CUSTOM_SEED) then
client.reboot_core()
else
hasAlreadyStartedPlaying = utils.ingame()
end
strategies.init(hasAlreadyStartedPlaying)
if (RESET_FOR_TIME and hasAlreadyStartedPlaying) then
RESET_FOR_TIME = false
print("Disabling time-limit resets as the game is already running. Please reset the emulator and restart the script if you'd like to go for a fast time.")
end
if (productionMode) then
bridge.init()
else
input.setDebug(true)
end
local previousMap
while true do
local currentMap = memory.value("game", "map")
if (currentMap ~= previousMap) then
input.clear()
previousMap = currentMap
end
if (not input.update()) then
if (not utils.ingame()) then
if (currentMap == 0) then
if (running) then
if (not hasAlreadyStartedPlaying) then
client.reboot_core()
hasAlreadyStartedPlaying = true
else
resetAll()
end
else
startNewAdventure()
end
else
if (not running) then
bridge.liveSplit()
running = true
end
choosePlayerNames()
end
else
local battleState = memory.value("game", "battle")
if (battleState > 0) then
if (battleState == 1) then
if (not inBattle) then
control.wildEncounter()
if (strategies.moonEncounters) then
strategies.moonEncounters = strategies.moonEncounters + 1
end
inBattle = true
end
end
local isCritical
local battleMenu = memory.value("battle", "menu")
if (battleMenu == 94) then
isCritical = false
elseif (memory.double("battle", "our_hp") == 0) then
if (memory.value("battle", "critical") == 1) then
isCritical = true
end
end
if (isCritical ~= nil and isCritical ~= criticaled) then
criticaled = isCritical
strategies.criticaled = criticaled
end
else
inBattle = false
end
local currentHP = pokemon.index(0, "hp")
if (currentHP == 0 and not strategies.canDie and pokemon.index(0) > 0) then
strategies.death(currentMap)
elseif (walk.strategy) then
if (strategies.execute(walk.strategy)) then
walk.traverse(currentMap)
end
elseif (battleState > 0) then
if (not control.shouldCatch(partySize)) then
battle.automate()
end
elseif (textbox.handle()) then
walk.traverse(currentMap)
end
end
end
if (PAINT_ON) then
paint.draw(currentMap)
end
input.advance()
emu.frameadvance()
end
bridge.close()

200
storage/inventory.lua Normal file
View File

@ -0,0 +1,200 @@
local inventory = {}
local pokemon = require "storage.pokemon"
local input = require "util.input"
local memory = require "util.memory"
local menu = require "util.menu"
local utils = require "util.utils"
local items = {
pokeball = 4,
bicycle = 6,
moon_stone = 10,
antidote = 11,
paralyze_heal = 15,
full_restore = 16,
super_potion = 19,
potion = 20,
escape_rope = 29,
carbos = 38,
repel = 30,
rare_candy = 40,
helix_fossil = 42,
nugget = 49,
pokedoll = 51,
super_repel = 56,
fresh_water = 60,
soda_pop = 61,
pokeflute = 73,
ether = 80,
max_ether = 81,
elixer = 82,
x_accuracy = 46,
x_speed = 67,
x_special = 68,
cut = 196,
fly = 197,
surf = 198,
strength = 199,
horn_drill = 207,
bubblebeam = 211,
water_gun = 212,
ice_beam = 213,
thunderbolt = 224,
earthquake = 226,
dig = 228,
tm34 = 234,
rock_slide = 248,
}
local ITEM_BASE = 0xD31E
-- Data
function inventory.indexOf(name)
local searchID = items[name]
for i=0,19 do
local iidx = ITEM_BASE + i * 2
if (memory.raw(iidx) == searchID) then
return i
end
end
return -1
end
function inventory.count(name)
local index = inventory.indexOf(name)
if (index ~= -1) then
return memory.raw(ITEM_BASE + index * 2 + 1)
end
return 0
end
function inventory.contains(...)
for i,name in ipairs(arg) do
if (inventory.count(name) > 0) then
return name
end
end
end
-- Actions
function inventory.teach(item, poke, replaceIdx, altPoke)
local main = memory.value("menu", "main")
local column = menu.getCol()
if (main == 144) then
if (column == 5) then
menu.select(replaceIdx, true)
else
input.press("A")
end
elseif (main == 128) then
if (column == 5) then
menu.select(inventory.indexOf(item), "accelerate", true)
elseif (column == 11) then
menu.select(2, true)
elseif (column == 14) then
menu.select(0, true)
end
elseif (main == 103) then
input.press("B")
elseif (main == 64 or main == 96 or main == 192) then
if (column == 5) then
menu.select(replaceIdx, true)
elseif (column == 14) then
input.press("A")
elseif (column == 15) then
menu.select(0, true)
else
local idx = 0
if (poke) then
idx = pokemon.indexOf(poke, altPoke)
end
menu.select(idx, true)
end
else
return false
end
return true
end
function inventory.isFull()
return memory.raw(0xD345) > 0
end
function inventory.use(item, poke, midfight)
if (midfight) then
local battleMenu = memory.value("battle", "menu")
if (battleMenu == 94) then
local rowSelected = memory.value("menu", "row")
if (menu.getCol() == 9) then
if (rowSelected == 0) then
input.press("Down")
else
input.press("A")
end
else
input.press("Left")
end
elseif (battleMenu == 233) then
menu.select(inventory.indexOf(item), "accelerate", true)
elseif (utils.onPokemonSelect(battleMenu)) then
if (poke) then
if (type(poke) == "string") then
poke = pokemon.indexOf(poke)
end
menu.select(poke, true)
else
input.press("A")
end
else
input.press("B")
end
return
end
local main = memory.value("menu", "main")
local column = menu.getCol()
if (main == 144) then
if (memory.value("battle", "menu") == 95) then
input.press("B")
else
local idx = 0
if (poke) then
idx = pokemon.indexOf(poke)
end
menu.select(idx, true)
end
elseif (main == 128 or main == 60) then
if (column == 5) then
menu.select(inventory.indexOf(item), "accelerate", true)
elseif (column == 11) then
menu.select(2, true)
elseif (column == 14) then
menu.select(0, true)
else
local index = 0
if (poke) then
index = pokemon.indexOf(poke)
end
menu.select(index, true)
end
elseif (main == 228) then
if (column == 14 and memory.value("battle", "menu") == 95) then
input.press("B")
end
elseif (main == 103) then
input.press("B")
else
return false
end
return true
end
return inventory

274
storage/pokemon.lua Normal file
View File

@ -0,0 +1,274 @@
local pokemon = {}
local bridge = require "util.bridge"
local input = require "util.input"
local memory = require "util.memory"
local menu = require "util.menu"
local utils = require "util.utils"
local pokeIDs = {
rhydon = 1,
kangaskhan = 2,
nidoran = 3,
spearow = 5,
voltorb = 6,
nidoking = 7,
ivysaur = 9,
gengar = 14,
nidoranf = 15,
nidoqueen = 16,
cubone = 17,
rhyhorn = 18,
gyarados = 22,
growlithe = 33,
onix = 34,
pidgey = 36,
jinx = 72,
meowth = 77,
pikachu = 84,
zubat = 107,
ekans = 108,
paras = 109,
weedle = 112,
kakuna = 113,
dewgong = 120,
caterpie = 123,
metapod = 124,
hypno = 129,
weezing = 143,
alakazam = 149,
pidgeotto = 150,
pidgeot = 151,
rattata = 165,
raticate = 166,
nidorino = 167,
geodude = 169,
squirtle = 177,
oddish = 185,
}
local moveList = {
cut = 15,
fly = 19,
sand_attack = 28,
horn_attack = 30,
horn_drill = 32,
tackle = 33,
thrash = 37,
tail_whip = 39,
poison_sting = 40,
leer = 43,
growl = 45,
water_gun = 55,
surf = 57,
ice_beam = 58,
bubblebeam = 61,
strength = 70,
thunderbolt = 85,
earthquake = 89,
dig = 91,
rock_slide = 157,
}
local data = {
hp = {1, true},
status = {4},
moves = {8},
level = {33},
max_hp = {34, true},
attack = {36, true},
defense = {38, true},
speed = {40, true},
special = {42, true},
}
local function getAddress(index)
return 0xD16B + index * 0x2C
end
local function index(index, offset)
local double
if (not offset) then
offset = 0
else
local dataTable = data[offset]
offset = dataTable[1]
double = dataTable[2]
end
local address = getAddress(index) + offset
local value = memory.raw(address)
if (double) then
value = value + memory.raw(address + 1)
end
return value
end
pokemon.index = index
local function indexOf(...)
for ni,name in ipairs(arg) do
local pid = pokeIDs[name]
for i=0,5 do
local atIdx = index(i)
if (atIdx == pid) then
return i
end
end
end
return -1
end
pokemon.indexOf = indexOf
-- Table functions
function pokemon.battleMove(name)
local mid = moveList[name]
for i=1,4 do
if (mid == memory.raw(0xD01B + i)) then
return i
end
end
end
function pokemon.moveIndex(move, pokemon)
local pokemonIdx
if (pokemon) then
pokemonIdx = indexOf(pokemon)
else
pokemonIdx = 0
end
local address = getAddress(pokemonIdx) + 7
local mid = moveList[move]
for i=1,4 do
if (mid == memory.raw(address + i)) then
return i
end
end
end
function pokemon.info(name, offset)
return index(indexOf(name), offset)
end
function pokemon.getID(name)
return pokeIDs[name]
end
function pokemon.getName(id)
for name,pid in pairs(pokeIDs) do
if (pid == id) then
return name
end
end
end
function pokemon.inParty(...)
for i,name in ipairs(arg) do
if (indexOf(name) ~= -1) then
return name
end
end
end
function pokemon.forMove(move)
local moveID = moveList[move]
for i=0,5 do
local address = getAddress(i)
for j=8,11 do
if (memory.raw(address + j) == moveID) then
return i
end
end
end
return -1
end
function pokemon.hasMove(move)
return pokemon.forMove(move) ~= -1
end
function pokemon.updateParty()
local partySize = memory.value("player", "party_size")
if (partySize ~= previousPartySize) then
local poke = pokemon.inParty("oddish", "paras", "spearow", "pidgey", "nidoran", "squirtle")
if (poke) then
bridge.caught(poke)
previousPartySize = partySize
end
end
end
-- General
function pokemon.isOpponent(...)
local oid = memory.value("battle", "opponent_id")
for i,name in ipairs(arg) do
if (oid == pokeIDs[name]) then
return name
end
end
end
function pokemon.isDeployed(...)
for i,name in ipairs(arg) do
if (memory.value("battle", "our_id") == pokeIDs[name]) then
return name
end
end
end
function pokemon.isEvolving()
return memory.value("menu", "pokemon") == 144
end
function pokemon.getExp()
return memory.raw(0xD17A) * 256 + memory.raw(0xD17B)
end
function pokemon.inRedBar()
local curr_hp, max_hp = index(0, "hp"), index(0, "max_hp")
return curr_hp / max_hp <= 0.2
end
function pokemon.use(move)
local main = memory.value("menu", "main")
local pokeName = pokemon.forMove(move)
if (main == 141) then
input.press("A")
elseif (main == 128) then
local column = menu.getCol()
if (column == 11) then
menu.select(1, true)
elseif (column == 10 or column == 12) then
local midx = 0
local menuSize = memory.value("menu", "size")
if (menuSize == 4) then
if (move == "dig") then
midx = 1
elseif (move == "surf") then
if (pokemon.inParty("paras")) then
midx = 1
end
end
elseif (menuSize == 5) then
if (move == "dig") then
midx = 2
elseif (move == "surf") then
midx = 1
end
end
menu.select(midx, true)
else
input.press("B")
end
elseif (main == 103) then
menu.select(pokeName, true)
elseif (main == 228) then
input.press("B")
else
return false
end
return true
end
return pokemon

132
util/bridge.lua Normal file
View File

@ -0,0 +1,132 @@
local bridge = {}
local utils = require("util.utils")
local client = nil
local timeStopped = false
local function send(prefix, body)
if (client) then
local message = prefix
if (body) then
message = message..","..body
end
client:send(message..'\n')
return true
end
end
local function readln()
if (client) then
local s, status, partial = client:receive('*l')
if status == "closed" then
client = nil
return nil
end
if s and s ~= '' then
return s
end
end
end
-- Wrapper functions
function bridge.init()
end
function bridge.tweet(message) -- Two of the same tweet in a row will only send one
print('tweet::'..message)
return send("tweet", message)
end
function bridge.pollForName()
bridge.polling = true
send("poll_name")
end
function bridge.chat(message, extra)
if (extra) then
print(message.." || "..extra)
else
print(message)
end
return send("msg", message)
end
function bridge.time(message)
if (not timeStopped) then
return send("time", message)
end
end
function bridge.stats(message)
return send("stats", message)
end
function bridge.command(command)
return send("livesplit_command", command)
end
function bridge.comparisonTime()
return send("livesplit_getcomparisontime")
end
function bridge.process()
local response = readln()
if (response) then
-- print('>'..response)
if (response:find("name:")) then
return response:gsub("name:", "")
else
end
end
end
function bridge.input(key)
send("input", key)
end
function bridge.caught(name)
if (name) then
send("caught", name)
end
end
function bridge.hp(curr, max)
send("hp", curr..","..max)
end
function bridge.liveSplit()
send("start")
timeStopped = false
end
function bridge.split(encounters, finished)
if (encounters) then
-- database.split(utils.igt(), encounters)
end
if (finished) then
timeStopped = true
end
send("split")
end
function bridge.encounter()
send("encounter")
end
function bridge.reset()
send("reset")
timeStopped = false
end
function bridge.close()
if client then
client:close()
client = nil
end
print("Bridge closed")
end
return bridge

119
util/input.lua Normal file
View File

@ -0,0 +1,119 @@
local input = {}
local bridge = require "util.bridge"
local memory = require "util.memory"
local lastSend
local currentButton, remainingFrames, setForFrame
local debug
local bCancel = true
local function bridgeButton(btn)
if (btn ~= lastSend) then
lastSend = btn
bridge.input(btn)
end
end
local function sendButton(button, ab)
local inputTable = {[button] = true}
joypad.set(inputTable)
if (debug) then
gui.text(0, 7, button.." "..remainingFrames)
end
if (ab) then
button = "AB"
end
bridgeButton(button)
setForFrame = button
end
function input.press(button, frames)
if (setForFrame) then
print("ERR: Reassigning "..setForFrame.." to "..button)
return
end
if (frames == nil or frames > 0) then
if (button == currentButton) then
return
end
if (not frames) then
frames = 1
end
currentButton = button
remainingFrames = frames
else
remainingFrames = 0
end
bCancel = button ~= "B"
sendButton(button)
end
function input.cancel(accept)
if (accept and memory.value("menu", "shop_current") == 20) then
input.press(accept)
else
local button
if (bCancel) then
button = "B"
else
button = "A"
end
remainingFrames = 0
sendButton(button, true)
bCancel = not bCancel
end
end
function input.escape()
local inputTable = {Right=true, Down=true}
joypad.set(inputTable)
bridgeButton("Escape")
end
function input.clear()
currentButton = nil
remainingFrames = -1
end
function input.update()
if (currentButton) then
remainingFrames = remainingFrames - 1
if (remainingFrames >= 0) then
if (remainingFrames > 0) then
sendButton(currentButton)
return true
end
else
currentButton = nil
end
end
setForFrame = nil
end
function input.advance()
if (not setForFrame) then
bridgeButton("e")
-- print("e")
end
end
function input.setDebug(enabled)
debug = enabled
end
function input.test(fn, completes)
while (true) do
if (not input.update()) then
if (fn() and completes) then
break
end
end
emu.frameadvance()
end
if (completes) then
print(completes.." complete!")
end
end
return input

138
util/memory.lua Normal file
View File

@ -0,0 +1,138 @@
local memory = {}
local memoryNames = {
setting = {
text_speed = 0x0D3D,
battle_animation = 0x0D3E,
battle_style = 0x0D3F,
yellow_bitmask = 0x1354,
},
menu = {
settings_row = 0x0C24,
column = 0x0C25,
row = 0x0C26,
current = 0x1FFC,
main_current = 0x0C27,
input_row = 0x0C2A,
size = 0x0C28,
pokemon = 0x0C51,
shop_current = 0x0C52,
transaction_current = 0x0F8B,
selection = 0x0C30,
selection_mode = 0x0C35,
scroll_offset = 0x0C36,
text_input = 0x04B6,
text_length = 0x0EE9,
main = 0x1FF5,
},
player = {
name = 0xD158,
name2 = 0xD159,
moving = 0x1528,
x = 0xD362,
y = 0xD361,
facing = 0x152A,
repel = 0x10DB,
party_size = 0xD163,
},
game = {
map = 0xD35E,
frames = 0xDA45,
battle = 0xD057,
textbox = 0x0FC4,
},
shop = {
transaction_amount = 0x0F96,
},
progress = {
trashcans = 0x1773,
},
pokemon = {
exp1 = 0xD179,
exp2 = 0xD17A,
exp3 = 0xD17B,
},
battle = {
confused = 0x106B,
turns = 0x1067,
text = 0x1125,
menu = 0x0C50,
accuracy = 0x0D1E,
x_accuracy = 0x1063,
disabled = 0x0CEE,
paralyzed = 0x1018,
opponent_move = 0x0FEE,
critical = 0x105E,
opponent_bide = 0x106F,
opponent_id = 0xCFE5,
opponent_level = 0xCFF3,
opponent_type1 = 0xCFEA,
opponent_type2 = 0xCFEB,
our_id = 0xD014,
our_status = 0xD018,
our_level = 0xD022,
our_type1 = 0xD019,
our_type2 = 0xD01A,
},
}
local doubleNames = {
pokemon = {
attack = 0xD17E,
defense = 0xD181,
speed = 0xD183,
special = 0xD185,
},
battle = {
opponent_hp = 0xCFE6,
opponent_max_hp = 0xCFF4,
opponent_attack = 0xCFF6,
opponent_defense = 0xCFF8,
opponent_speed = 0xCFFA,
opponent_special = 0xCFFC,
our_hp = 0xD015,
our_max_hp = 0xD023,
our_attack = 0xD025,
our_defense = 0xD027,
our_speed = 0xD029,
our_special = 0xD02B,
},
}
local function raw(value)
return mainmemory.readbyte(value)
end
memory.raw = raw
function memory.string(first, last)
local a = "ABCDEFGHIJKLMNOPQRSTUVWXYZ():;[]abcdefghijklmnopqrstuvwxyz?????????????????????????????????????????-???!.????????*?/.?0123456789"
local str = ""
while first <= last do
local v = raw(first) - 127
if v < 1 then
return str
end
str = str..string.sub(a, v, v)
first = first + 1
end
return str
end
function memory.double(section, key)
local first = doubleNames[section][key]
return raw(first) + raw(first + 1)
end
function memory.value(section, key)
local memoryAddress = memoryNames[section]
if (key) then
memoryAddress = memoryAddress[key]
end
return raw(memoryAddress)
end
return memory

193
util/menu.lua Normal file
View File

@ -0,0 +1,193 @@
local menu = {}
local input = require "util.input"
local memory = require "util.memory"
local YELLOW = GAME_NAME == "yellow"
local sliding = false
-- Private functions
local function getRow(menuType, scrolls)
if (menuType and menuType == "settings") then
menuType = menuType.."_row"
else
menuType = "row"
end
local row = memory.value("menu", menuType)
if (scrolls) then
row = row + memory.value("menu", "scroll_offset")
end
return row
end
local function setRow(desired, throttle, scrolls, menuType, loop)
local currentRow = getRow(menuType, scrolls)
if (throttle == "accelerate") then
if (sliding) then
throttle = false
else
local dist = math.abs(desired - currentRow)
if (dist < 15) then
throttle = true
else
throttle = false
sliding = true
end
end
else
sliding = false
end
return menu.balance(currentRow, desired, true, loop, throttle)
end
local function isCurrently(desired, menuType)
if (menuType) then
menuType = menuType.."_current"
else
menuType = "current"
end
return memory.value("menu", menuType) == desired
end
menu.isCurrently = isCurrently
-- Menu
function menu.getCol()
return memory.value("menu", "column")
end
function menu.open(desired, atIndex, menuType)
if (isCurrently(desired, menuType)) then
return true
end
menu.select(atIndex, false, false, menuType)
return false
end
function menu.select(option, throttle, scrolls, menuType, dontPress, loop)
if (setRow(option, throttle, scrolls, menuType, loop)) then
local delay = 1
if (throttle) then
delay = 2
end
if (not dontPress) then
input.press("A", delay)
end
return true
end
end
function menu.cancel(desired, menuType)
if (not isCurrently(desired, menuType)) then
return true
end
input.press("B")
return false
end
-- Selections
function menu.balance(current, desired, inverted, looping, throttle)
if (current == desired) then
sliding = false
return true
end
if (not throttle) then
throttle = 0
else
throttle = 1
end
local goUp = current > desired == inverted
if (looping and math.abs(current - desired) > math.floor(looping / 2)) then
goUp = not goUp
end
if (goUp) then
input.press("Up", throttle)
else
input.press("Down", throttle)
end
return false
end
function menu.sidle(current, desired, looping, throttle)
if (current == desired) then
return true
end
if (not throttle) then
throttle = 0
else
throttle = 1
end
local goLeft = current > desired
if (looping and math.abs(current - desired) > math.floor(looping / 2)) then
goLeft = not goLeft
end
if (goLeft) then
input.press("Left", throttle)
else
input.press("Right", throttle)
end
return false
end
function menu.setCol(desired)
return menu.sidle(menu.getCol(), desired)
end
-- Options
function menu.setOption(name, desired)
if (YELLOW) then
local rowFor = {
text_speed = 0,
battle_animation = 1,
battle_style = 2
}
local currentRow = memory.raw(0x0D3D)
if (menu.balance(currentRow, rowFor[name], true, false, true)) then
input.press("Left")
end
else
local rowFor = {
text_speed = 3,
battle_animation = 8,
battle_style = 13
}
if (memory.value("setting", name) == desired) then
return true
end
if (setRow(rowFor[name], true, false, "settings")) then
menu.setCol(desired)
end
end
return false
end
-- Pause menu
function menu.isOpen()
return memory.value("game", "textbox") == 1 or memory.value("menu", "current") == 24
end
function menu.close()
if (memory.value("game", "textbox") == 0 and memory.value("menu", "main") < 8) then
return true
end
input.press("B")
end
function menu.pause()
if (memory.value("game", "textbox") == 1) then
local main = memory.value("menu", "main")
if (main > 2 and main ~= 64) then
return true
end
input.press("B")
else
input.press("Start", 2)
end
end
return menu

68
util/paint.lua Normal file
View File

@ -0,0 +1,68 @@
local paint = {}
local memory = require "util.memory"
local player = require "util.player"
local inventory = require "storage.inventory"
local pokemon = require "storage.pokemon"
local encounters = 0
function elapsedTime()
local secs = memory.raw(0xDA44)
if (secs < 10) then
secs = "0"..secs
end
local mins = memory.raw(0xDA43)
if (mins < 10) then
mins = "0"..mins
end
return memory.raw(0xDA41)..":"..mins..":"..secs
end
paint.elapsedTime = elapsedTime
function paint.draw(currentMap)
local px, py = player.position()
gui.text(0, 14, currentMap..": "..px.." "..py)
gui.text(0, 0, elapsedTime())
if (memory.value("battle", "our_id") > 0) then
local hp = pokemon.index(0, "hp")
local hpStatus
if (hp == 0) then
hpStatus = "DEAD"
elseif (hp <= math.ceil(pokemon.index(0, "max_hp") * 0.2)) then
hpStatus = "RED"
end
if (hpStatus) then
gui.text(120, 7, hpStatus)
end
end
local nidx = pokemon.indexOf("nidoran", "nidorino", "nidoking")
if (nidx ~= -1) then
local att = pokemon.index(nidx, "attack")
local def = pokemon.index(nidx, "defense")
local spd = pokemon.index(nidx, "speed")
local scl = pokemon.index(nidx, "special")
gui.text(100, 0, att.." "..def.." "..spd.." "..scl)
end
local enc = " encounter"
if (encounters ~= 1) then
enc = enc.."s"
end
gui.text(0, 116, memory.value("battle", "critical"))
gui.text(0, 125, memory.value("player", "repel"))
gui.text(0, 134, encounters..enc)
return true
end
function paint.wildEncounters(count)
encounters = count
end
function paint.reset()
encounters = 0
end
return paint

38
util/player.lua Normal file
View File

@ -0,0 +1,38 @@
local player = {}
local textbox = require "action.textbox"
local input = require "util.input"
local memory = require "util.memory"
local facingDirections = {Up=8, Right=1, Left=2, Down=4}
function player.isFacing(direction)
return memory.value("player", "facing") == facingDirections[direction]
end
function player.face(direction)
if (player.isFacing(direction)) then
return true
end
if (textbox.handle()) then
input.press(direction, 0)
end
end
function player.interact(direction)
if (player.face(direction)) then
input.press("A", 2)
return true
end
end
function player.isMoving()
return memory.value("player", "moving") ~= 0
end
function player.position()
return memory.value("player", "x"), memory.value("player", "y")
end
return player

52
util/settings.lua Normal file
View File

@ -0,0 +1,52 @@
local settings = {}
local memory = require "util.memory"
local menu = require "util.menu"
local YELLOW = GAME_NAME == "yellow"
local settings_menu
if (YELLOW) then
settings_menu = 93
else
settings_menu = 94
end
local desired = {}
if (YELLOW) then
desired.text_speed = 1
desired.battle_animation = 128
desired.battle_style = 64
else
desired.text_speed = 1
desired.battle_animation = 10
-- desired.battle_style =
end
local function isEnabled(name)
if (YELLOW) then
local matching = {
text_speed = 0xF,
battle_animation = 128,
battle_style = 64
}
local settingMask = memory.value("setting", "yellow_bitmask")
return bit.band(settingMask, matching[name]) == desired[name]
else
return memory.value("setting", name) == desired[name]
end
end
function settings.set(...)
for i,name in ipairs(arg) do
if (not isEnabled(name)) then
if (menu.open(settings_menu, 1)) then
menu.setOption(name, desired[name])
end
return false
end
end
return menu.cancel(settings_menu)
end
return settings

57
util/utils.lua Normal file
View File

@ -0,0 +1,57 @@
local utils = {}
local memory = require "util.memory"
function utils.dist(x1, y1, x2, y2)
return math.sqrt(math.pow(x2 - x1, 2) + math.pow(y2 - y1, 2))
end
function utils.ingame()
return memory.raw(0x020E) > 0
end
function utils.each(table, func)
for key,val in pairs(table) do
func(key.." = "..tostring(val)..",")
end
end
function utils.eachi(table, func)
for idx,val in ipairs(table) do
if (val) then
func(idx.." "..val)
else
func(idx)
end
end
end
function utils.match(needle, haystack)
for i,val in ipairs(haystack) do
if (needle == val) then
return true
end
end
end
function utils.key(needle, haystack)
for key,val in pairs(haystack) do
if (needle == val) then
return key
end
end
end
function utils.igt()
local secs = memory.raw(0xDA44)
local mins = memory.raw(0xDA43)
local hours = memory.raw(0xDA41)
return secs + mins * 60 + hours * 3600
end
function utils.onPokemonSelect(battleMenu)
return battleMenu == 8 or battleMenu == 48 or battleMenu == 184 or battleMenu == 224
end
return utils