Skip to content

Latest commit

 

History

5 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

AI Rivals

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

Contents


Install

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).


What the rivals do

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.


The Pokégear

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.


Relationships, careers and stories

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.


Tournaments, the League and the title

  • 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.

Double battles and Wonder Trade

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_battles 0.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.


Options

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.


Compatibility

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.


Your save is not touched

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.


Performance

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.guard wraps 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.


Debugging

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.


Tests

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.lua

38 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.py

Static 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.


Repository layout

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/.


Known limitations

  • 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.

Credits and licence

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.

About

AI Rivials for gen1recomp

Resources

Stars

4 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages