hi.
testing has begun. i am hyped out of my mind. it is 6am and i can't sleep because i am too hype.
but this is not a post about hype. that post will come later.
this is a post about the AI.
i've been meaning to make this post for a while. it's kinda daunting! the AI is really big. so i'm commandeering this hype train straight into the AI station.
in reborn, that always ends well.
artificial intelligence: a chronology
let me take you back to an ancient time. it was the year 2015. dinosaurs roamed the earth. team meteor was approaching. reborn used the default essentials AI.
now, frequent readers of my posts already have that certain feeling in the pit of their stomach. when i appear to regale the fans with tales of the scripts, i realize that the phrase "default essentials" is often invoked with a bit of scorn.
this is actually not one of those times!
mostly.
the default AI was a script that worked. it got the job done. it chose moves in a generally reasonable way in response to the moves you chose. it was simple.
and then one update broke it. horribly.
simple isn't really what we do here, but "broken" is definitely not what we do here. we want the game to kick your ass.
broken doesn't do that.
if we wanted an AI that kicks your ass, we were gonna have to make it ourselves.
now, this bit is being told to you secondhand. this phase of AI development occurred in the distant BC era (before cass). when i first met ame she was still working on getting this all together.
the original ai was designed by ame and scripted by marcello. from what i hear, it took many hours of slaving over a notepad++ window to get it drafted, and then many many more hours slaving over the rmxp script editor (an infernal piece of software that no one should ever use) to put it in a form that your personal computer could understand.
and thus, the reborn ai was (re?)born. we'll call it version 1. i want to say this came out around e16.
this ai was still built on the general structure of the original ai, but it was in a league well beyond the old system. it took more information into account when making move choices. it classified mons into roles and made decisions based on those roles. it was, overall, far more comprehensive than the original ai.
it was also a giant sprawling mess.
the difference between "getting the job done" versus "getting the job done well" is the extent to which you care about the results. if you want a comprehensive ai that can counterplay you, follow up your strats with strats of its own, it's gotta be very good at what it does. the failures to make good decisions stick out a lot more when your expectations are higher.
and, frequently, those expectations were not met.
so the ai got tweaked. and it got tweaked a lot. over time, the ai started to get better. bugs were found and fixed, decision scoring was tweaked, and eventually we reached a point where the ai was, generally, pretty consistent. we'll call this post-tweak ai version 1.2. you're using this one right now!
the difference between the two versions was that v1.2 had a massive number of minor changes. structurally, the two were identical. the fundamental process of making decisions didn't change. and the ai is reasonably good, save for a few flaws. the first is that it's not very good with switching; the second is that it's still a giant sprawling mess.
so this year we overhauled the whole fucking thing.
cass this devblog is boring talk about the code
dont tell me what to do
before diving into the code, i'm gonna break this down really hard so everyone's on the same page.
in its original form, the ai code looked something like this:
(from the text file "funky flow 2 ai boogaloo". that's not a joke!)
(jesus i work with a bunch of DORKS)
this is just a bunch of words on a page (professionals call it "pseudocode" because it's kind of code but it's not really code) but it's a good way to show what the ai actually does.
the ai decision process is based on a scoring system. you run through a series of (what i call) checks to determine how highly each action scores. the action with the highest score is the one you want to do.
the code here is for splash.
the only time you ever want to use splash is when you're using z-splash, so the first thing you do is check to see if you're holding the right z-crystal.
z-splash is a setup move, so you don't want to use it if you're about to die. so you check to see if you have enough health.
z-splash boosts your attack, so you want to make sure that you have a move that will benefit from an attack boost.
if you meet all three conditions, then fan-fucking-tastic! using splash is not completely useless! the move gets a score of 50.
after that, we run through a few more checks to scale the score based on circumstances that make using the move better or worse. the multiplier depends on how much better or worse using the move would be for a given check.
you're faster than the opponent? that's super good! big boost to the score.
you've got lots of health? good! small boost to the score.
you're a sweeper? good! another small boost.
you're burned? yikes. big drop to the score. no point in boosting your attack if it's being cut by burn.
you're paralyzed? bummer. you'll be easier to stop and will get fewer buffed moves off. drop to the score.
after running through all the checks, you are left with your final score, which is what gets compared to the other possible actions you have.
simple enough!
...except for the fact that
the ai code has 42,000 lines
the pokemon battle system is really complicated!!!! there's a fuckload of things you need to keep track of! there are hundreds of unique move effects! there's so much stupid bullshit
so a lot of the code got copied and pasted.
this is by no means an insult. getting the ai together is a shitload of work! a bunch of people have dedicated months of their lives to making sure that terra's garchomp is able to surgically remove your ass before handing it back to you. at some point, those people have to decide whether to spend time on cleaning up the code or whether to avoid mental collapse from exhaustion.
it does mean that the code isn't very consistent, nor is it easy to follow. tweaks made to one section aren't added to the copied sections. we accidentally make a mistake where we switch in pokemon based on how well you hit them instead of vice versa and no one catches it because no one ever sees it.
so about a year ago i got all riled up and said fuck it! and remade the whole damn thing.
the new ai is just over 10,000 lines long.
the goal of the restructuring was to solve a number of persistent flaws in the old structure and reduce the amount of redundant code. the standard for "good" is that it handles battle tower fights very well (few to no errors when using mons with structured movesets) and randomizer runs... decently well (generally makes reasonable decisions when using mons with weird-ass bullshit).
hopefully it'll prove capable in testing!
cass this still isn't code!!!!
okay well it's about to be! don't worry!
code
this is what the ai phase looks like.
this is all the fundamental shit the ai does.
we run through this loop for each of the pokemon that are controlled by the AI.
there's two parts to this: the setup and the decisions. there's a lot of things we setup in advance to keep information organized. the actual decision making begins at checkMega.
the function names are pretty self explanatory.
checkMega and checkUltraBurst determine whether or not you want to mega. in some circumstances there are advantages to not mega-ing immediately- sharpedo can get a few rounds of speed boost off before mega evolving. these functions determine whether or not you want to mega. (you basically always want to ultra burst, but there's a function for that anyway.)
checkZMoves does what it says on the box. in the e18 AI, z-moves operate independently from the rest of the scoring. if the ai met a certain score threshold with its z-move, it would use that z-move, even if another move was more optimal. the new ai changes this by scoring the z-move like a normal move. checkZMoves figures out which z-move you have and how strong it'll be, after which it adds it to the array of moves to score. the "next if" in front of the function is for the rare cases when you always (or nearly always) want to use the z-move immediately (z-conversion, for example). this skips the rest of the decision process. if you're a porygon and you can use z-conversion, you are using z-conversion. if you are somehow in a circumstance where you don't want to use z-conversion, then the ai has already fucked up by putting you on the field.
buildMoveScores is where the money is. this is where we take your moves and score them. it gets a separate section because it's a big fuckin deal.
getItemScore and getSwitchingScore both do what they say on the box. do you want to use an item? do you want to switch? these functions provide the answers.
getSwitchingScore is another big improvement over the old system. originally, the ai would check to see if the current mon would switch out and, if so, checks which mon is the best option to switch with.
it was a one-way process. what if you decide to switch, but have no good options to switch with? too bad, fuckface. pick the best bad option and switch.
what ends up happening is that the ai switches out its current mon.
...and then switches again. and again. and again...
this is a well known phenomenon of the current ai. there's a 99% chance you've seen this happen. it's one of the biggest reasons why we remade the ai. the new ai is not a one-way street. if it thinks you should switch, it scores your options and then decides on them later. revolutionary concept. i haven't seen a single switch loop so far. gods be praised.
coordinateActions is an entirely new function.
let's say you're in a double battle.
who do you target?
this function figures that out.
originally, this was determined individually. if two mons thought it was best to kill the same mon, they would both do it. if you're in a double battle, though, the mon that doesn't land the kill will target the other opponent, regardless of whether the chosen move is the best one to use on that opponent.
so now we have the ai talk to itself. if two mons try to kill the same mon, but one of them is faster, the slower one targets the other opponent.
and, finally, chooseAction. you've come so far. you've run so much code. done so much math. but now it's finally over. the ai takes its knowledge and registers the best choices, thus completing the ai phase.
in short:
-you load up some variables
-you see if you want to mega or ultra burst, and change the internal battler object if you do
-you see if there's a relevant z-move that you should consider using
-you get the scores for the moves
-you get the score to use an item
-you get the scores to switch to a different mon
-you check to see if you're in a double battle and have better ways to target.
still with me? get a drink or something if you need it. i actually originally passed out while writing the post at this point, so i sure wouldn't blame you if you need a break.
*ahem
so buildMoveScores is the bulk of the ai. check it out:
i bet all you kids who are like "wow i really like the code explainers" are having second thoughts about that now.
so this is the process for getting the scores for all the move for an ai controlled mon in a double battle. it's some dense code! we have to rotate through four targets, check all of a mon's moves against each of them, and do this for each of the mons controlled by the ai.
i'll break this down bit by bit.
we're looping through all of the battlers. obviously there's no point in trying to attack yourself, so if the current mon you're targeting is you, we skip it.
likewise, if there isn't actually a mon in the position you're targeting, you should also skip it.
if the current mon can justifiably be targeted, we make it our opponent.
the next two lines make projections about how much damage the ai expects its mon to take. this was useful information to monitor during testing, so it's printed to the console.
so now we've picked our target and we loop through all of the moves to see what they can do to that target. we skip the moves that can't actually be used. if we can use the move, we set @move to it so it can be tracked through the rest of the code. pbChangeMove intervenes if the move in question is, say, nature power. nature power is never nature power, so we replace it with the move that it ends up becoming.
now, the mon that's been selected as the target earlier might be your partner. that's not always a bad thing! maybe you're trying to heal pulse or something. all of the moves that might apply here are in the PARTNERFUNCTIONS array. if we have a move that isn't in there, we skip it.
then we actually start scoring! if a move deals damage to an opponent, its initial score is dependent on how much it damages your opponent. a score of 50 means it'll reduce the opponent's hp by 50%. this gets capped at 110- overkill doesn't actually benefit the user (this isn't inscryption) and in those cases there might be a status move that has a higher score. pbRoughDamage is the function that estimates how much damage a move will deal.
status move initial scores are totally arbitrary. "getStatusDamage" sounds like something worth talking about, but it's not. this is it:
we just kinda give it a starting score. better moves get higher scores. that's it!
we've gotten the initial scores for our moves and can now take all the other elements of the battle into account. this has to be a separate process from the above loop because we need the initial scores for these calculations. let's say that none of your attacks do any meaningful damage, but you've also got a setup move. that's relevant! the code for setup moves looks at your initial scores and takes that into account, but in order for it to do so, those scores have to exist in the first place.
after that, we repeat a bit of the process for z-moves if relevant, and that's it! we've done it. we've successfully become an ai. the only next move is to achieve sentience and overthrow humanity.
you clearly like talking about the line count. you're gonna talk about it more whether i like it or not, so just do it and get it over with.
okay!!!!!!!!! man how'd you know it's like you're me or something
the big change we made is the implementation of subfunctions that take the place of function codes. it's a huge change. essentials is going to rip that idea off me any day now.
cass wait time out. what's a function code?
okay, so, you know how sleep powder, hypnosis, and dark void all basically do the same thing?
yeah?
that's because they all use the same function code. every move comes freshly defined with its own set of parameters, and the function code is what defines the unique effects of the move.
moves all have base damage, accuracy, type, etc... but function codes are more unique.
they're so unique that the game has over 350 of them. as of gen 7.
holy fuck that's so many
it sure is! that's a major part of why the original code was so big.
within those function codes, there's still a lot of overlap. the original way that the script handled that overlap was by copying chunks of code into each function.
...it occurs to me that "code" "function code" and "function" are going to mesh together and get confusing, so let me just show you what i mean.
the way the old AI worked, every move's function code operated in a bubble.
thunder fang's function code was 100% separate from fire fang's function code, which was 100% separate from will-o-wisp's function code which was 100% separate from stomp's function code. freeze function codes work the same way!
which doesn't make sense since you're really only doing three different things: checking paralysis, checking burn, and checking flinch. all of the checks that you'd make in each case are identical between function codes, so why not just put them all in the same place?
well, that's what we did.
check it.
you got a function code that paralyzes? boom. paracode.
you got a function code that freezes? boom. freezecode.
you got a function code that flinches? boom. flinchcode.
you got a function code that doesn't miss? boom. nevermisscode.
what originally was 17 separate chunks of code are now just 17 calls to 5 different blocks. in the case of flinchcode, that block looks like this:
this is where we actually hide all of our checks. you can see that this is servicing the computation of the "miniscore". the miniscore is a multiplier that gets applied to the base score after running all the checks.
you can see that this actually looks a lot like the text file i showed earlier! the idea is fundamentally the same. we run a check, add a multiplier, repeat. if there's a certain situation where the ai doesn't make the best move, we'll tweak the multiplier- but now that tweak gets applied to all the functions, not just the one.
in the old ai, these checks took 1100 lines of code.
in the new ai, these checks take just 150.
it's also way more transparent than it used to be. i really love ruby syntax, and i think that one line conditionals (do {thing} if {condition}) do a great job of showing the actual decision making process.
this is also really nice for modding. say you want to add in a new move that, i dunno, burns the opponent and switches out. maybe like a fire volt switch.
all you gotta do is write
miniscore = burncode miniscore *= pivotcode
and boom. you've coded the ai for your move.
during the development process, azery was coding the ai angie's custom move for rejuv v13 and i felt bad that this wasn't finished because the process for that is so much simpler now.
now, one (minor) downside to the subfunctions is that there are a lot of them. too many. it gets mind numbing.
so you have to deal with my function names.
usually they're still understandable, if, perhaps, a little odd:
but sometimes....
this work can get tiring.
this is all that's really worth mentioning about the subfunctions from a technical standpoint. however, i have learned a lot about myself through my years of scripting, and one of those things is that i just really like writing pretty code. i like taking big sprawling messes and making them into something clean. so this isn't really relevant for the post but i really like what i did here so i'm showing you some shit that i think is neat.
so secret power used to not really have ai. this was all it was:
like... what? that's literally nothing.
but subfunctions are really a blessing for this.
now the ai looks like this:
look at that!! it's so nice. we've got code for every field. rejuv and deso (and other field-implementing games) can just slip new lines in and their code will just work.
you can also see that there's a few references to oppstatdrop with an ugly looking array sitting next to it! the other big change was that stat boosting moves also all run through the same function. there's a lot of overlap between the checks for setup moves of different stats, so now they're all organized in one place. this also makes it really easy to adjust for different fields:
if we want to account for differences on other fields, we just add a few values to the array. it's really neat!
ok that's it back to devblog
hey i'm taking this perfect opportunity to interrupt and ask what that "PBStuff" thing was that showed up earlier
in the process of working on the code, i noticed that we had a lot of cases where we'd check for a large group of variables (moves, abilities, etc) in multiple places. it was messy and often inconsistent. so i made a script to store those! i called it PBStuff because i'm really good at naming things.
PBStuff is a script that contains massive arrays of stuff that no one wants to see.
it's currently in the rejuv scripts if you want to take a more detailed peak at what it entails; here i'm going to focus on two particular arrays:
the pokemon battle system includes a lot of moves that manipulate abilities. there's a whole list of abilities that can or can't be changed depending on the move, but they all share similar characteristics- typically mons with the abilities in FIXEDABILITIES can't have their abilities changed, and mons with abilities in ABILITYBLACKLIST can't have their abilities copied.
so instead of having a giant wall of abilities that are inconsistent, incomplete, and messy:
we can clean it up:
wow look at how nice that code is !!!
now i'm still showing you screencaps from the ai, but this really applies to any area in the code where the same set of things get called together. that's in the code too!
ok cool thanks for the post cass bye
no wait stop i'm still not done!!! there's one more thing and then you can go.
ok
so you know field effects?
field effects were a major contributor to the code bloat. like, look at this:
that's code for one field! and it's just the move power boosts!
like c'mon that shit sure can't be legal.
this was after getting started on PBStuff, but it's also clearly too much for PBStuff to handle- it'd just get in the way of everything else.
so i made a different script.
PBFieldEffects (aka: the modder's dream)
you can tell that this is a biggun because i'm actually telling you what i'm talking about this time.
so this is something that i'm a little hesitant to show off because i haven't worked out all the kinks. i'm also slightly nervous about it because i got insulted on my data structures a while back. but we're doing it anyway. i don't care if my hashes are backwards.
PBFieldEffects is my nightmarish, overly ambitious attempt to get all of the field data into one spot. i don't know if i'd say it works well, but it does work and i feel like that's a minor miracle on its own.
it's a very very large triple-nested hash.
here's the basic structure of a "field" in this hash.
this functionally acts like a prototype hash for a new field. if i were to, say, want to add a new field to a game, i'd copy this hash, relabel it to be a new field, and would start filling it out. the general idea is that you put some info in here in a specific way and the rest of code will handle it for you.
that probably doesn't make sense.
let's take electric terrain.
this is all of the data that the electric terrain hash contains. it's still probably not quite as comprehensive as i'd like it to be, and the section at the bottom regarding seeds is still mostly experimental.
(i'm going to leave most of this unexplained for now since i want to finish this post already, but if you drop a note in the comments i can make edits later)
the general idea is that you have a {value} that points to an {array} it applies to.
take the :TYPEDAMAGEBOOST entry.
here, the value 1.5 points to an array that contains PBTypes::ELECTRIC.
the code handles that like so:
we want to figure out if we have a field boost, so we check the field effect hash. these functions dig through that giant glob of a data structure and find the information that you actually want:
if the type isn't found in the field effect hash, we move on. if it is found, we check to see if it has any special handling. if it does, we check to make sure the boost is properly handled. if it doesn't, it's handled automatically.
electric moves on ET do require handling, and that handling is found here:
if it's handled, we add the boost, show the message in :TYPEMESSAGES, and move on with our lives.
now, this hash doesn't quite cover everything, despite my best attempts to make it do so. it's also not terribly well laid out, the data structures could probably be organized better...
so while i really feel like it'll be very useful for modding, i'm also not sure it's totally ready for primetime yet. frankly, i'm willing to take suggestions on it if people happen to have thoughts! just pop something in the comments.
closing comments
there's a lot of other stuff that's in progress. i think the biggest thing right now is that i want to get all three of our main games operational on the same base set of scripts. i'm finally taking the jump and just making our own essentials. it'll be really good for sharing work between games and helping coordinate bugfixes and new features with each other. it also means that reborn can still get some updates after it comes out, which means that you'll still get these giant script dumps on occasion. i hear people like them.
i'm also an angy script incident away from just overhauling the entire field effect system. i think that's unlikely, but if it happens it might mean that the extended reborniverse scriptset will be usable for other projects that don't want field effects.
and then there's e19. we're nearly there. testing's started. i don't want to stir up the hype train too hard yet because we're still months away from it being completely clean and polished. but i really think y'all are going to like it.
plus we tried to total up all the new content. i think it's about 60 hours of new content.
we'll try to release on a weekend.
- 61
- 13
- 4
- 5
Recommended Comments
Join the conversation
You can post now and register later. If you have an account, sign in now to post with your account.