Persistent AI trainers who play the game alongside you — for Pokémon Red, Blue, Yellow and Gold on Gen1Recomp.
They are not scripted encounters. Each rival owns a party, a badge case, money, a personality and a location, and advances all of them on its own clock whether or not you are looking. They route across the real map graph, train on real encounter tables, queue at gym doors, lose gym battles, catch Pokémon, black out to Poké Centers, form opinions about you and about each other, and turn up to fight you with whatever team they actually have at that moment.
CHRIS TRAINING ROUTE 4 4 badges CHARMELEON Lv31
DYANARI SEEKING_GYM CELADON CITY 3 badges RAICHU Lv29
MATT HEALING CERULEAN CITY 3 badges NIDOKING Lv28
LARISSA LEAGUE_READY INDIGO PLATEAU 8 badges VENUSAUR Lv46
- Install
- What the rivals do
- The Pokégear
- Relationships, careers and stories
- Tournaments, the League and the title
- Double battles and Wonder Trade
- Options
- Compatibility
- Your save is not touched
- Performance
- Debugging
- Tests
- Repository layout
- Known limitations
- Credits and licence
Copy the ai_rivals/ folder into the engine's mods/ directory, then enable it
from the in-game mod manager or the launcher's mod panel.
| Platform | Path |
|---|---|
| Windows | %APPDATA%\pokemon-love2d\mods\ai_rivals\ |
| Linux | ~/.local/share/love/pokemon-love2d/mods/ai_rivals/ |
| macOS | ~/Library/Application Support/LOVE/pokemon-love2d/mods/ai_rivals/ |
| Android / iOS | the engine's save directory, mods/ai_rivals/ |
Everything must sit under one top-level ai_rivals folder —
ai_rivals/manifest.json, ai_rivals/main.lua, and so on. There are no hard
dependencies, nothing to build, and no engine patch: this is a stock API 2
mod.
The manifest carries a github field, so the launcher's built-in updater will
offer new releases once the mod is installed.
Starting a game. On a new save the mod asks how many rivals should walk the world — anything from one to all 100. Four is the default, and the authored quartet (CHRIS, DYANARI, MATT, LARISSA) always fills the first slots; beyond them the roster deals from a 100-strong content pool whose personalities and starters are chosen by the world seed, so no two playthroughs field the same cast. Starters are random between saves and fixed within one — reloading never changes who has what.
To uninstall, delete the folder. See Your save is not touched for what that leaves behind (nothing that matters).
They live on a tick, not on your screen. One core.update hook feeds a
single accumulator; a tick runs every 1.5 seconds and advances at most two
rivals. A rival on your map is a real NPC — sprite, collision, walk animation,
talkable. A rival anywhere else is a row in a table with a map id and a hop
clock: no grid movement, no pathfinding, no drawing. That split is the whole
reason this runs on a phone.
They progress for real. Levels come from real experience against real encounter tables. Catches need an encounter table and a Poké Ball the rival actually bought with its own money. Evolutions are level evolutions the species really has. Gym battles are resolved against the leader's real party — and a rival that walks in underlevelled loses, goes and trains, and comes back.
They are bounded by your progress. A rival may not run more than one gym ahead of you, and trains toward the next leader's level plus a personality margin. The Speedrunner's margin is 0, so it arrives underprepared and sometimes loses; the Strategist's is 7, so it rarely does. Party size grows with badges: two slots before the first badge, one more per badge after, capped at six and further limited by personality.
They fight you when they want to, not on a trigger. A rival on your map may keep travelling, wander, train, or challenge you. It never chases you across Kanto, and never engages on cooldown, with an empty party, or while healing or defeated. When it does engage you fight its live party — the CHARMELEON Lv31 above, because that rival really has one. Beat it and its party faints and it blacks out to the nearest Poké Center, exactly as yours would. A rival that beat you gains confidence; a rival that lost goes and trains rather than quietly getting stronger.
They talk. Press A at a rival for a compact six-row conversation menu —
challenge (1V1 or 2V2), trade, travel together, gifts, and a RIVAL INFO
submenu holding mood, story, battle style, career, weather preferences and how
they feel about you. Every line is data in data/dialogue.lua, keyed by
situation and personality with a default arm, and drawn from a stream seeded
off the rival's state — so chatting to someone never changes what they decide to
do next.
The world reports on them. A rate-limited news ticker carries badge wins, notable catches, tournament results, League runs and rivalries. Sightings tell the truth: the mod reports where a rival was actually seen, never where it is.
A full Pokégear lives in the START menu — on Red, Blue and Yellow as well as Gold.
| Screen | What it does |
|---|---|
| CLOCK | In-game and device time |
| PHONE | Contacts, calls and call history |
| WEATHER TRACKER | Regional weather, live when Weather FX is installed |
| RANKINGS | Every trainer in the save on one table — including you |
| RADIO | Pokémon Music, Trainer Radio, Pokédex Radio, and custom music |
The phone is a real network. Ordinary overworld trainers — discovered from the loaded game's own trainer data, gyms and Team Rocket excluded — may offer you their number after you beat them. Numbers are deterministic and save-stable, capacity is 200, and accepted contacts call you with rematch requests, gossip, rival sightings and weather reports. Rematches use the engine's native trainer battle construction rather than a scripted shortcut.
Calling a rival is a conversation, held in the engine's own text box rather than on a ticker you cannot see from four menus deep:
- COME HERE — the rival routes to your map on foot, and says so. It does not teleport, and a refusal is spoken rather than swallowed.
- CHALLENGE — staged, then launched once the menus are actually closed.
- TOURNAMENT — arrange one at your current location.
- ASK ABOUT — what this rival honestly knows about another's team, derived from their head-to-head record: the full party after three battles, the lead and the ace after one or two, and only badges and a sighting with none.
Rankings and records. RANKINGS puts you on the same table as everyone else, ordered by the same comparison, so the one question the screen exists to answer — am I winning? — can be read straight off it. Selecting a trainer opens their record: badges, Elite Four progress, title defences, favourite Pokémon, career, rivalries and highlights. All of it is derived from the world rather than logged alongside it, so taking a badge away takes the sentence about it away too.
Custom music. Drop .ogg, .mp3, .wav or .flac files into
custom_music/, or into subfolders for playlists, and pick one from
RADIO → CUSTOM MUSIC. The mod stays inside the API 2 sandbox and deliberately
does not read arbitrary folders elsewhere on your computer.
Every rival holds a bounded, persistent bond toward you and toward each other rival: affection, respect, grievance and a head-to-head record, all moved by things that actually happen — battles, gifts, trades, favours, snubs, cooperation, life-path events.
States are derived, never assigned. Competition → Rivalry → Friendship → Hatred, refined by NEMESIS, DEVOTED, PROTEGE, ADMIRER and ESTRANGED. Nothing rolls a die for a relationship; each refined state is a strict subset of the one it refines, so it can only be reached by earning it. Every state has its own dialogue — a state a rival cannot say anything about is a label in a menu, not a relationship.
LOVERS is a pair state — neither rival can decide it alone, and it appears only when both have independently reached DEVOTED. Rivals in a mutual pair actually travel to each other's maps, gated so that being in love never interrupts a gym battle, a heal or a League run. UNREQUITED names the near miss; FEUD, CLOSE_FRIENDS, ARCH_RIVALS and MENTORSHIP fill out the rest.
Life paths and careers. Rivals drift off the badge treadmill into Breeder, Researcher, Explorer or Retired, with workplaces matched to real maps and ranks that take real time. A rival carrying a genuine grievance — against you or against another rival — may fall in with Team Rocket; doubt and redemption are derived from behaviour rather than granted on a timer, and a Rocket rival gains nothing for the turn.
- Rival-only tournaments run autonomously; results and eliminations appear in the news.
- Player tournaments are arranged by phone or face to face, 3–100 entrants capped by the rivals your save can actually field, with an explicit STANDARD / DOUBLE battle-mode screen. Battles award double EXP, winning awards a rare prize, and you may hold one per local calendar day — the used day is persisted, so reloading cannot farm it. Bracket state survives a save and safely resumes a pending match after reload.
- The League. Rivals qualify, run the Elite Four, and can take the Champion title. While the crown is held, clearing the Elite Four makes a rival a challenger instead, and the title only ever changes hands in a title match — the defence recorded on one side, the attempt on the other, and a beaten challenger loses its qualification. One clear buys one shot.
- A title held by you is never settled by the simulation. That one is fought in person.
Both are bundled, both were written by other people, and both were absorbed with the author's permission given directly — see CREDITS.md.
- Double battles (shanehudson's
double_battles0.5.0) provides targeting, turn order, switching, the wide HUD, animation offsets, spread moves, trainer pairs and the 3D adapter. Rival challenges and tournaments can both be fought 2v2. - Wonder Trade provides the attendant, the pool and the trade, rebuilt here as an atomic transaction.
Installing AI Rivals replaces the standalone versions of both, which is why the manifest declares them as conflicts. Disable them before enabling this.
| Row | Values | Effect |
|---|---|---|
RIVALS |
OFF / TWO / ALL | Roster fallback for saves with no chosen count; OFF also hides the menu row |
RIVAL DUELS |
on / off | Whether rivals may challenge you |
WORLD PACE |
SLOW / NORMAL / FAST | 3.0s / 1.5s / 0.6s per tick |
WALKING COST |
LOW / BALANCED / FULL | How much on-screen movement detail rivals get |
RIVAL BONDS |
on / off | Relationships and the states derived from them |
RIVAL SIGHTINGS |
on / off | Sighting reports through the phone and the news |
RIVAL LIFE PATHS |
on / off | Careers, Team Rocket and life-path drift |
NEWS TICKER |
on / off | The world news line |
LK DEBUG |
on / off | Debug counters |
A RIVALS row in the START menu shows each rival's badges, location, status and party.
Games. Red, Blue, Yellow and Gold. The game is detected from the content of the loaded dataset rather than from a version string, so a randomizer or a total conversion is handled by the same code: Yellow is recognised by its surfing Pikachu, Gold by its own map and trainer tables, and anything unrecognised falls through to the Gen 1 arm rather than to a wrong answer. Everything a roster row points at is resolved against the loaded game — a species, portrait or AI class this boot does not have is omitted rather than failing the load.
Gold needs no engine patch. Player duels ride the cart's own trainer battle:
CHRIS and DYANARI shadow the story rival classes so every scripted encounter
fights their live party, and the rest are registered as fresh classes. Talking to
a rival on Gold goes through src/Gen2Talk.lua, a single router over the one seam
Gold provides, so two features can never fight over the A button.
Sprite replacement mods are authoritative. AI Rivals claims and repaints no player, rival, trainer or NPC artwork. Walkers are resolved from the merged runtime sprite table every time a rival spawns, preferring vanilla logical ids — so a replacement mod's art is simply used. Overhaul mods that rename ids instead of replacing them are discovered semantically, and a total conversion with no familiar ids gets a deterministic live fallback. Battle portraits reference host trainer classes, leaving replacement portraits authoritative.
Optional interop — detected at runtime, never bundled, never required.
Everything below is asked for lazily through the API that mod publishes, inside a
pcall, and every missing, broken or unknown case is treated as off.
| Mod | What it adds |
|---|---|
| Weather FX | The sky over your map is real to rivals standing on it: typed weather shifts confidence, and gym or ghost fights under your sky run with that weather applied |
| Kanto-Reforged | The dex scope is respected — asked live on every starter, catch and evolution target, and it can only ever remove |
| HGSS_SPRITES | Extra species and sprite sources merged into the roster and the Wonder Trade pool |
| silph-scope | Rivals detour to reachable ghost maps and fight them; the result is local and nothing is written into silph's storage |
| Dramatic Shape / voxel forks | The 3D battle surface the double-battle adapter poses two Pokémon on |
| wild_skies | Supplies the visible bird a wild double can start against |
Conflicts: double_battles, wonder_trade — both are bundled here.
The mod writes no vanilla save field. Everything lives in its own namespaced
bucket, which the engine routes to save.modData.ai_rivals — data only, no
metatables, no functions, no engine references.
That isolation is enforced by the test suite rather than promised: it scans every
source file, comments stripped, for assignments to save.badges, save.party,
save.inventory, save.flags[…], save.money and save.pokedex. Reading your
party to decide whether to pick a fight is allowed; writing anything is not.
Trainer classes are new ids (OPP_AIR_*), never reshaped vanilla ones, and rival
badges are an entirely separate store from yours.
Delete the folder and the base game loads normally — the unknown modData bucket
is ignored. A party slot naming a species the current boot does not have is
dropped on restore, so removing a Pokémon-adding mod between saves costs a slot
rather than crashing a battle, and a missing or corrupt bucket re-seeds the
roster.
Designed for a low-powered Android handheld.
- Tick-based, never per frame — a full second of 60fps frames does zero simulation work, which the suite asserts directly.
- A bounded per-tick budget: adding a fifth rival costs nothing per tick, it just means each rival is visited less often.
- At most one simulated gym battle per tick across the whole roster.
- A* only for on-screen rivals, only on route invalidation; routes cached per pair.
- No allocation on the update path when no tick is due.
- A stall (an app suspend, a long load) drops its backlog rather than replaying it — a ten-minute stall costs one tick, not four hundred.
Util.guardwraps every entry point: a raise costs that rival that tick, is counted, and never reaches your error feed or drops a frame.
Pure Lua 5.1. No shell commands, no native libraries, no OS APIs, no networking,
no threads, no absolute paths. File access goes through mod:read, which works
identically from a folder install and from a packed .modpkg.
Developer builds only (love . --developer or POKEPORT_DEV=1).
| Verb | Does |
|---|---|
rivals |
One line each: state, map, destination, badges, lead level, route length |
rival_list |
The same, column-aligned |
rival_tp <id> <map> |
Move a rival |
rival_givebadge <id> <badge> |
Grant a badge |
rival_setstate <id> <state> |
Force a state |
rival_simulate [n] |
Run n ticks immediately (default 50) |
rival_reset |
Re-seed the roster |
Every rival owns a 32-bit LCG seeded from data/rivals.lua and saved with its
state, so the same save replays to the same world — rival_simulate produces
identical results twice in a row, which is what makes any of this debuggable.
77 suites, all headless: no ROM, no LÖVE, no graphics context.
39 Lua suites run under plain lua5.1 from the engine root:
lua5.1 mods/ai_rivals/tests/ai_rivals_test.lua38 Python static contracts pin structural facts a runtime test cannot reach — that the save isolation holds, that a state test comes before the state it refines, that a save version has not regressed:
python mods/ai_rivals/tests/story_static.pyStatic contracts assert the floor at which a feature shipped rather than a literal version, so a test fails when a feature regresses and not when the version number moves.
ai_rivals/
manifest.json id, version, targets, conflicts, permissions
main.lua every engine seam, the module loader, and no decisions
data/ rivals · personalities · gyms · dialogue · notable
src/ the thinking: Rival, AI, Simulation, BattleSim, MapGraph,
Pathfinder, Relationships, LifePath, Career, Story, League,
Tournaments, Economy, Pokegear, Phone, Comms, Scout,
DoubleBattles, WonderTrade, Weather, Sprites, ...
lib/ small shared helpers
assets/fx/ the handful of effect sprites the mod does ship
custom_music/ drop your own music here
docs/ architecture, performance notes, per-release changelogs
tests/ 39 Lua suites + 38 Python static contracts
main.lua owns every engine seam and decides nothing; the modules decide
everything and touch no engine API. The engine loads one chunk per mod, so
main.lua brings a 20-line require built on mod:read, and every module opens
with local req, mod = ....
Adding a rival is one entry in data/rivals.lua — no new file and no new branch.
Adding a personality is one entry in data/personalities.lua; dialogue falls back
to the default arm until lines are written for it.
Deeper documentation — the architecture, the save format, the full release history
back to 1.7.0 — is in docs/ai-rivals.md. Release notes are
in CHANGELOG.md, with the full per-release notes under
docs/changelogs/.
- League runs are simulated, not story events. A rival that becomes Champion does not displace your Elite Four or rewrite any vanilla flag; the title lives in this mod's own save bucket.
- Catching needs an encounter table. No table for a map, no catch — rather than inventing a species.
- Stone and trade evolutions do not happen. Level evolutions only; a rival has no bag and no trade partner.
- Battles you never see are estimated. Gym, League and skirmish results come from the estimator; every battle you are actually in uses the real engine.
- Rival-vs-rival fights are off screen. You will not watch two rivals battle; the result and the money that changes hands are applied, and they carry on.
- Talk scripts are Gen 1 only. Gold reimplements map scripting without the registry, so Gold uses its own interaction route (see Compatibility).
- The suite is fixture-based, since the engine ships no ROM-derived data. It covers logic thoroughly and cannot cover real Kanto map geometry.
Two of this mod's systems were written by other people and absorbed whole, each with its author's permission given directly rather than inferred from a licence: double battles by shanehudson, and Wonder Trade. CREDITS.md is the full record, including the mods this one integrates with rather than bundles.
Pokémon is a trademark of Nintendo; the Gen 1 and Gen 2 games are © Nintendo / Creatures Inc. / GAME FREAK inc. This is an unofficial fan mod. It ships no ROMs and no copyrighted game content — every sprite, name and table it uses is read at runtime out of your own cartridge dump.