As per the poll I placed a bit earlier, I will be discussing Pokemon following Object Oriented principles. These are the modern principles you could use when doing Object-Oriented programming in a language like Java or C#. In particular, I will focus on a battle or even more specifically a turn within the context of a battle and how one might implement this.
Of course, Pokemon games weren't (and most likely still aren't) implemented like this but I am merely asking the question of how one could implement this using modern Object Oriented principles. I wish to use this as an instrument to teach you about programming using objects. Naturally, there are many ways to do this and this isn't the "one true" method of doing it. This will be heavily (almost an exact copy of) based on a wonderful video by Christopher Okhravi called "Rebuilding Pokémon with Object Oriented Programming". His video is probably better than my explanation, so go watch it.
First some explanations about various terms from OOP that I will be using. Object-oriented programming is a way of organizing code by thinking about the world in terms of "things" (objects) rather than just a list of instructions. We do this by defining classes, which are like blueprints that describe what something should look like and what it can do without actually giving details.
From these classes we can obstruct objects which are the actual things that you create from that blueprint. When you create a new object from a class, we call this instantiation. So for example you can have some kind of Pokemon class and from that create a Charmander object. When we want to share common traits, we can use subclasses (also known as a child or derived class) which lets us create more specific versions from a more general one (like making a subclass for Fire-type pokemon based on a class Pokemon).
An interface is like a contract. It says that "anything that follows me must be able to do these specific things". For example, an Evolvable interface might require that any Pokemon implementing that interface be able to evolve. This promotes polymorphism (letting classes implement the same interface in different ways) which is nice for reusable code. This also helps with various design patterns. For example, certain IEffects require an INumber. They do not care how you get that INumber or what it is (i.e. it can be gotten via a constant or a random number or the experience), as long as it is an INumber. We'll get to this later.
Lastly, I talk a lot about fields and methods. Fields are just variables that belong to a class or object. Methods are functions that apply a class or object. So a field could be a number to represent HP, while a method could be a function like evolve(). We start with our first, highest class, "Battle". Perhaps, more aptly, this class could be called "Turn." A Battle must have at least two objects, one for the players pokemon and one for the encounter, be that from a trainer or a wild encounter. We could thus name them "encounter" and "player", or we could call them "attacker" and "defender".
This second pattern would introduce a kind of directionality because after this turn, we would have to switch "attacker" and "defender" so the previous defender becomes the new attacker and vice-versa. There are many ways to do this, each of which introduces wonderfully diverse implications but we'll just keep it as "attacker" and "defender'. We could then have a method "swap(Battle): void" which swaps attacker and defender, making use of Mutation. Alternatively, we could make the function "swap(Battle): Battle". This takes the path of immutability, where we take in a battle and out comes the next Battle, instead of having 1 single battle that we keep changing (aka mutating).
Battle might also have many other objects and methods like the name of the encounter trainer and the player trainer name, the items in the bag and so on but we'll ignore that for now.
We'll say attacker and defender both have the type Battler. This Battler type should have values for name: String, hp: Integer, elements. For elements we can either make it a list of Type, Type[] or we can make it something like a Pair<Type> where Type is an enum (i.e. enum Type { NORMAL, FIRE, WATER, GRASS, ELECTRIC, …, FAIRY }). We can also create a custom class 'record TypePair(Type primary, Type secondary?)' with which we can add methods for calculating super-effectiveness and whatnot. These are all implementation details, so they don't matter too much. I just thought it would be nice to mention.
Lastly, a Battler must have a set of Moves. In particular, we will define an interface called IMove, so Battler has object "moves: List<IMove>".
An interface is basically an object without methods. In effect, an interface is a structure that enforces certain properties on classes that implement that interface. It portrays the idea of every subclass, basically.
In this case, IMove is the idea of a move. A class that implements IMove must have a method called "execute(Battle): void" (Again we have this discussion of mutation vs immutability). IMove just specifies that, in order to be a move, you have to be able to execute it using the context of a Battle. We are following object-orientented principles, so we won't make an individual move for every single move possible but instead want to be able to express every single possible move.
We shall define three (3) subclasses for IMove: Move, WithApplicability and WithPrecondition. The main bread and butter for IMove is Move. This class will implement IMove and it will hold objects like "name: String", "element: Type", "attempt: IAttempt" (we'll get to that in a bit).
The subclasses WithPrecondition and WithApplicability will serve to add conditions to a move. We will give both of them an ICondition (Again, we'll get to that). ICondition will be a generic type, meaning it will take some kind of type and return a boolean (True/False) based on the inputted type. WithPrecondition will have an ICondition<Battle> (Meaning an ICondition that takes in a Battle) and WithApplicability will have an ICondition<Battler> (Meaning an ICondition that takes in a Battler). Both WithPrecondition and WithApplicability will also have an object of type IMove. Meaning a WithCondition object can hold a WithApplicability object which in turn holds a Move object.
WithPrecondition is for moves that might require certain prerequisites. For example, a move might not even be allowed to start based on the current preconditions like weather or having to recharge after hyper beam.
WithApplicability is for moves that might not always be legal to execute, so you can try to execute and you'll lose your turn on that basis but the move might not actually do anything as, for example, you could be paralyzed, or the pokemon you're attacking is immune to the moves element. It's not a miss and you're allowed to start the turn, but it does nothing (or less damage).
WithPrecondition, WithApplicability and Move are IMove's. However, WithApplicability and WithPrecondition HAVE an IMove as well, while Move is an IMove but does not have an IMove on its own. They are wrappers; they are decorators. They wrap a move with some kind of condition. This creates a very flexible system where you can have any number of simple effects and you can "compose" them together, to create a very nuanced move.
Now we have to explain IAttempt and ICondition. We'll start with IAttempt. IAttempt is, once again, an interface. It represents the idea of an attempt within the context of a move, so when a move is being executed. IAttempt once again has a method "execute(Battle): void". It also has subtypes Attempt, Cascade and Combo.
Let's first look at Attempt. First off, Attempt has an animation object. I won't specify the type as it's really not necessary for this discussion. Up next it has an accuracy object of type IEffect<Battle>. So once again, this ICondition returns us a boolean when we give it a battle, so it can spin up a random number generator or whatnot. Our accuracy is expressed as a condition (and of course our ICondition type can express a lot more things than just probability but this is most common).
Next, Attempt has 3 objects of type IEffect: 'onHit', 'onMiss' and 'after'. When we do an attempt and it actually hits, we can call the effect of 'onHit'. When it misses we can call 'onMiss' and we always call 'after'. This seems like quite a bit, but it's actually necessary because there's just so many different moves and some use all of these things.
We can, of course, add optional types when designing the constructor of these types. It would be very awkward if we had to specify 'onMiss' and 'after' even nothing should happen in those cases. To remedy this, we can have constructor overloading and make multiple (static) constructors of creating an IAttempt where onMiss or after defaults to no effect. We could even default 'accuracy' to be 100% and make even more constructors.
Now, I won't say we should make these defaults null (so expressing onMiss = null or attempt = null) when we want to have no effect. Instead, I think we should apply the null-object pattern where we make an instance of IEffect that represents no effect. We'll look at that later.
We have now defined the Attempt type, so an attempted strike. The other two subclasses we defined were Cascade and Combo. Combo will be for 2-5 hit moves like Bullet Seed or Water Shuriken, while Cascade would be for chain of responsibility-like moves like Fury Attack.
Cascade will be very simple. It will implement IAttempt as usual and it will have an object of type List<IAttempt>. This means that it will hold a list of attempts, like an Attempt, each with their own animation and accuracy. We could add fields for 'perHitEffect: IEffect' (a way of increasing or applying more damage each successive hit) and an 'afterAllHits: IEffect' for effects like King's Rock or Razor Fang Flinching or whatnot. The reason we call it a cascade instead of a sequence is because Pokemon does not have a move with multiple attempts which continues after one one hit misses (as far as I know), so like a cascade, it quits after the first miss.
Combo will be more tricky as it is more of a counterpart to Attempt. Combo always means 1 attempt, 1 roll of a dice. It has one accuracy and one animation. Either it hits or it doesn't, but if it hits you can have X number of effects. To that end, Combo will have a field 'hits: INumber' that means how many hits the damage will do. It will also have an 'every: IEffect'. Once again, the hits field is used to figure out how many times to play the animation and how many times to use the effect. 'every' is the effect that happens every time you hit.
We said hits is an INumber. We'll talk about that in a bit. The point is that INumber is a number that only comes to be after you give it a Battle as context. This lets us be very flexible in the sense that we can have the number be dependent on, say, the level of the attacker or the weather or whatever. We can't specify a number, but we can specify how to compute that number.
Now, let's talk about conditions before talking about effects. Before we talk about the things that can happen, we have to talk about the necessary conditions for these things to happen. As I mentioned previously, we have an interface called ICondition. This is a generic type, meaning that it takes in another type and depending on that type, the methods and calculations and whatnot change.
The interface ICondition<T> (Something between triangular brackets < & > indicates a type. We use T to denote any generic Type) has to have a method 'check(T): bool'. So it takes in a type, and it returns a boolean. So we can have an ICondition<Battle> that takes in a Battle and returns a boolean. We can also have an ICondition<Battler> that takes in a Battler. We can do the same with INumber and so on.
We will also say ICondition<T> will have combinator methods, like 'And<T>', 'Or<T,>', 'Not<T>' and 'Probability'. Again these are methods that take in a type. The methods 'And', 'Or' and 'Not' are IConditions but they also have an ICondition (I don't think this makes sense if you haven't actually programmed using an OO language but oh well!). If we're expressing conditions, we need to be able to combine different predicates to create more complex predicates. The way to do that is by chaining logical operators like and, or and not.
For example, an 'And' condition for the type argument, T, would have to contain two other conditions also for T and check those individually and only if both of those are true, does it return true. It is similar for 'Or', where instead of having both be true, either one being true is enough to return true. 'Not' would take in one condition, check that and then report the inverse of it; it inverts the output of checking its condition. We could also call 'And' 'Both' and 'Or' 'Either' but that's semantics and I don't support it.
Lastly, Probability is here to express some kind of probability. In the constructor, you might pass a double like 0.5, meaning 50% of the time this Probability class will return true and 50% of the time it returns false.
Now, we can introduce various types of IConditions. First up, ICondition<INumber>. These are conditions that take in an INumber and have an INumber as well (So for example we could have class 'GreaterThan extends ICondition<INumber>', which we can then create an object for using "ICondition<INumber> gt = new GreaterThan(0.25)" and then we could check against an hp using "gt.check(Battle.hp)"). In this example, we'd need a class called GreaterThan which extends ICondition<INumber>. It can become quite tedious to make a class for every operation so in actuality it's probably easier to use a builtin Predicate thing or to use a factory class (i.e. "Num.gt(0.25).check(hp)").
In any case, a class is instantiated with some kind of INumber and then it has a method that uses that number and the newly given input number and returns a boolean as a result. Using this system of classes, we can make quite a complex condition. Again, the exact implementation details don't really matter (partially because I haven't implemented it myself yet).
If you want to do this using pass-around functions, you can do that as well and you wouldn't need the added complexity of implementing ICondition<INumber> as you could solve those problems on the fly using functions, but if we want to do this via object-oriented programming, we need this sort of plumbing to be able to solve these simple problems.
Next we have conditions for Battle and Battler. The former would take a Battle and then resolve to true or false. Similarly the latter would take a Battler and resolve to true or false. Subclasses of ICondition<Battler> could be HasElement or isParalyzed. These are conditions that check whether a Battler satisfies a certain property. It would work something like passing a Battler into isParalyzed, which then checks if it has this major status condition and returns true or false.
ICondition<Battle> is a bit interesting. This is a condition that takes in a Battle and returns True or False. One example I would make is adding a subclass called For which would have an ITarget field. In this case, we are doing something called Lifting (a principle very common in category theory or functional programming). The idea is that For can lift a Battler condition up to the level of a Battle condition.
Let's pause for a moment and discuss ITarget first. ITarget is unrelated to conditions. We'll use it later for Conditions. This is an interface that says to be a Target, you have to have a method "Resolve(Battle): Battler". It's, in essence, a selector. We could have a subclass Attacker which returns the currently attacking Battler and we could have a subclass Defender which returns the currently defending Battler.
Now, how do we use this? If we have an instance of a For object, then that must contain an ITarget. So you would say For(Attacker) or For(Battler) and then that For type would not only contain that Target, but also a Battler condition. What this means is we can lift a Condition for Battlers up to the level of a Battle condition. The reason we do this is so we only have to specify Battler Conditions for both parties; we only want to specify them once. We do not want a isAttackerParalyzed and a isDefenderParalyzed, we want just one isParalyzed. So we can combine, or compose, these Battler conditions with the For type to create a Battle condition. We want to work on the level of Battle. We want to pass in a Battle and from that figure out which particular Battler that condition should hold for.
Up next are the effects. These are the true heart of a battle. These are the effects that regulate hp and stats or whatever. All effects shall implement the IEffect interface. This is an interface that says that, to be an effect, you have to implement a method "Apply(Battle): Void". Once again, we're mutating the effect into the battle.
We already briefly discussed an effect when we were talking about the null-object pattern. The first effect that implements IEffect is NoEffect. This is literally just an object that indicates to do nothing, like Splash. So instead of passing around null to indicate no effect, we pass around the NoEffect object.
There are a bunch of effects and we, once again, want to be able to combine them. Therefore, we have a bunch of leaves ("pure" effects) and a bunch of decorators/combinators. We'll begin with the combinators. We have Sequence and we have Condition.
Sequence is very simple. It is an IEffect, but it also holds a list of effects. The Apply method would then probably just apply every effect in that list. This differs from Cascade from previous as, there, the idea was that if one attempt fails, the cascade ends. Here, however, we just keep applying the following effects regardless. We can model multiple effects as a single effect that's just a sequence.
The next combinator, Condition, is basically just an if-statement. This is not about whether you hit or miss a move. This is more for a move where sometimes something happens and other times it doesnt. Like flinching or quick claw. We have 3 objects: "onPass", "onFail" and "condition: ICondition<Battle>". If the condition passes, then we run the effect that's stored in onPass. Otherwise we'd run the effect in onFail.
Now there are a bunch of leaves left. The building blocks for making effects. These are things like NoEffect, Faint, Drain, FormulaDamage, OHKO, DirectDamage, Heal, Paralysis, AttackStatChange and so on. Each of these will take some kind of ITarget and possibly an INumber
We could have done this differently. We could have designed this in such a way to minimize the amount of effects or a language that uses as few combinators as possible. Instead, we're being more verbose to make the expressing feel as natural as possible. We could create more fundamental effects (like for example combining DirectDamage and OHKO as they're very similar).
The problem with this approach being that, while you save space, eventually you are going to be very verbose and the more moves you have, the more combinators you could need and at some point you're just designing a programming language which is not what we want to be doing here. We need a lot of classes, but it's more ergonomical and more pleasant to create more effects and moves. We want something pleasant, not elegantly minimal.
I think we should focus on FormulaDamage. As we know, the process of calculating damage in Pokemon is quite complex, dealing with the types of the attacker and the defender, the attack (or special attack) stat vs defense (or special defense) and factoring in weather and STAB and so on. This is a large formula but it is standardized and many moves follow it (though not all), so to abstract this away, we put it in the FormulaDamage.
So we can have a move that consists of several attempts and each attempt has it's own effect and one of those effects could be FormulaDamage. FormulaDamage has only one object: An INumber. It has an INumber because we have to say how much damage it makes, but we can't necessarily say that up front as it might depend. This is why we add this level of indirection. Notice how we do not specify an ITarget as we make the invariant assumption that this effect will always come from the Attacker and does damage to the Defender.
DirectDamage, then, completely bypasses the FormulaDamage and does some amount of damage to either the Attacker or the Defender (so it holds an ITarget object). The amount of damage would be stored in an INumber.
Lastly, we have to discuss INumbers. We've been passing around INumbers instead of just doubles or floats. We add this level of indirection/abstraction because in many cases, we don't know the number up front, but when given a Battle, we can know that number. Thus, any number that implements INumber must have the method "Evaluate(Battle): double".
Again, we have combinators and then some more pokemon-specific sublasses and some more general sublasses. The combinators each are INumbers but they also have an INumber. These are subclasses like Sum, Product, Quotient and so on. You instantiate such a class with a number, and every time you evaluate it with some number, you get some number created from the inputted number and the stored number (via i.e. summing them up, multiplying, dividing, etc.).
We also have the Constant subclass. This takes a double during instantiation and every time you evaluate it, you just get that stored value out. We can then also make a subclass Between, which takes two numbers during construction and when you evaluate it, you get a random number in between them. We can also make an equivalent class but for weighted random numbers.
The more pokemon specific INumbers will each take in an ITarget. They're all numbers that act on a specific target. We can also do the same as we did previously with the lifting thing, but I didn't think of that. So instead of being INumbers based on a Battle, they're based on a Battler and then you'd lift that up to the level of a Battle number.
Some examples are classes like MaxHP which would check the max HP of the target passed in during instantiation. We can also do CurrentHP, Level, LastDamageDealt. Each are pretty self explanatory.
Finally, we get to an actual bit of code/example. I won't go into this too much and especially not how to actually implement this but oh well. Let's just model the move Tackle:
new Move( "Tackle", Element.Normal, new Attempt( new TackleAnimation(), new Probability(1.00f), new FormulaDamage( new Constant(40) ) ) )











