Author Topic: [FF7] AI Template Project - Welder  (Read 33047 times)

Bosola

  • Fire hazard!
  • *
  • Posts: 1752
    • View Profile
    • My YouTube Channel
[FF7] AI Template Project - Welder
« on: 2010-04-30 22:45:44 »
CAUTION: The templates here are old. For the latest plugins, and an easy-to-use AI building environment, try out my 'parser', which comes with the latest versions of each template.  Click for more info.

Edit: Rewritten because it was, well, a bit of a muddle

OVERVIEW

Welder is an AI template based around a 'header', a 'footer' and a series of independent 'plugins'. The header will pick an attack, but x/yths of the time consult a series of plugins, that might decide to choose a different attack if their conditions are matched. In either case, the game will then reach a 'footer', that performs the action either the header or the plugins have stored in memory.

This way, enemies can simulate smart decisions, with different priorities and quirks, but not act 'telepathically'.

Users will need to copy the header, and then copy the plugins they want to use - the lowest priority decisions coming first. Once all the plugins they require have been copied into Proud Clod / Wallmarket, it's then a matter of typing a 'footer', which is just the opcode 92.

The structure of AI scripts produced by Welder might look like the following -

** header : Choose claw attack,
** header : make smart decision instead 2/3rds of the time -
  * Plugin - If enemy is in regen, cast dispell
  * Plugin - If enemy is critical, kill
  * Plugin - If ally is critical, heal
** footer : use the spell or attack that has been chosen

In this case, the monster - let's call him Gordon - will use a claw 1/3rds of the time. But, 2/3rds of the time he'll consider other possibilities - healing allies, snatching opportunities to KO enemies and destroying buffs in that priority, should those conditions be met (otherwise, he'll carry on using his claw).

FOR USERS

Don't care about AI code, just want to be able to easily customize it? Well, that is what Welder is for. You need to copy the HEADER -

Code: [Select]

12 2070
02 20a0
82
90
60 20
61 index of the 'dumb' attack

81
60 X
34
60 Y
43
70 ZZZZ

Where: Y+1 / X is the chance of making a 'smart' decision (you can make different enemies more or less intelligent than one another) and ZZZZ is the address of the 'footer' (see above), which is merely the opcode 92 at the end of the script. Put the index of the attack you'll use by default into the first instance, insert your preferred 'IQ', and then find the footer location, replacing ZZZZ with it.

Between the header and footer, you can paste in 'plugins', which are sections of code I and (I hope) others will post in this thread. Each plugin is self-contained, and caters to a specific circumstance and response - for instance, casting a spell when an ally is below 50% health, or using a particular spell on oneself (say, reflect) if enemies are in a certain status (in this case, the reflect status).

Once you've finished copying in the plugins you want for your monster (and customizing a few values - these will be marked up in the plugins), you need to create an opcode 92, which comes right at the end of the AI script (before 73). Easy, huh?

FOR DEVELOPERS

Ok. Here's the idea:

The header leaves two values on the stack: 20 and the index of the 'default' or 'dumb' attack. Plugins each check for conditions, and if these are met, the index is popped and replaced by the index of a new attack. The target data is changed too. If these conditions are not met, the stack goes on untouched. At the end of each plugin, no matter what happens, there will always be on the stack

*20
*a valid attack index

In summary
  • Header writes data for primary attack
  • Rolls a dice to decide whether it will use this attack, or consult the plugins - which might but don't have to change the attack to something more 'tactically' suitable
  • Performs whatever attack is left on the stack


The 'rules' of the plugins are:
* Note the length of the plugin, and the amount we will have to add, in hex, to the current address to reach the next plugin (and mark this up clearly within your code)
* Always leave the stack with exactly two values on it - the 20 needed for enemy attacks, and a 0-FFFF attack index.
* You can use as much internal logic as you like, just leave the stack 'clean'.
* Plugins respond to one particular condition with one particular attack. Keep it simple so that people can chop and change to avoid redundant data or alter the priorities an enemy has.
* Try to keep your plugins short and sweet. Space can be important.
* Plugins are self-contained. It's important that this template be orthogonal, and therefore easy to chop and change.

The idea is that newbies can simply take the header, runner, and the plugins that are appropriate to the creature they're thinking of, and still create a smart but not 'telepathic' monster who is fun to play against.

In summary:

Copy the header, modify the attack indexes and target data to your liking
Drop in the plugins
Create a 'runner' which is Opcode 92


So, shall we get posting?

See this page for an excel spreadsheet with templates
« Last Edit: 2014-07-17 18:21:48 by Covarr »

gjoerulv

  • *
  • Posts: 1225
  • me
    • View Profile
    • My Youtube
Welder: A little AI Template Project
« Reply #1 on: 2010-05-01 01:34:15 »
Basically what you need is to make 2 variables (at the very least): 1 for attack and 1 for target. If the condition changes you change the value of the variables. You should then end the code by setting the target and the attack, and finally some sort of check if thee code should loop. A really good ff7 AI should have as few random factors as possible. Just random enough to not make it predictable.

Imo a good template.

secondadvent

  • *
  • Posts: 287
    • View Profile
Welder: A little AI Template Project
« Reply #2 on: 2010-05-01 02:12:32 »
...
12 2070
02 20a0
82
90
60 20
61 FFFF

81
60 02
34
70 XXXX

//these following three lines are redundant/useless as the above already chose a random target, and the one
//below doesn't even set the target to the new random target (missing 90... did you just c/p and forget to
//delete that part? :-P).

//12 2070
//02 20A0
//82
91
61 EEEE
...

the way i have been writing the AI lately has made as much use of the stack as possible to reduce the overall size by a good bit the larger the AI is, but it becomes harder to keep track of, especially since the way i have been coding causes PrC to spit out errors and give up disassembling it :-P.

Edit: adding comments within the code may also help it to become clearer, rather than just placing it all and then commenting after, and explains things line-by-line rather than as a whole (you could still keep your summary afterwards, but comments make things easier to understand). not saying that the current layout isn't good as it is, but newcomers to the AI may have more trouble understanding without "holding their hand" through it.
« Last Edit: 2010-05-01 02:18:36 by secondadvent »

Covarr

  • Covarr-Let
  • Administrator
  • *
  • Posts: 3941
  • Just Covarr. No "n".
    • View Profile
Welder: A little AI Template Project
« Reply #3 on: 2010-05-01 02:18:02 »
Waitaminnit...

50% chance of a smart attack, 50% chance random?

This is basically assuming that all enemies are the same intelligence level. Wouldn't it make more sense to be able to adjust these odds per enemy? I mean, it seems like Reno would be a hell of a lot more likely to choose an intelligent attack than Hell House.

Bosola

  • Fire hazard!
  • *
  • Posts: 1752
    • View Profile
    • My YouTube Channel
Welder: A little AI Template Project
« Reply #4 on: 2010-05-01 11:24:52 »
Edit: This refers to a slightly different version of the header. I decided to remove randomization out of the header to improve space, and because it's possible to write plugins that randomly choose attacks anyway.

Basically what you need is to make 2 variables (at the very least): 1 for attack and 1 for target. If the condition changes you change the value of the variables. You should then end the code by setting the target and the attack, and finally some sort of check if thee code should loop. A really good ff7 AI should have as few random factors as possible. Just random enough to not make it predictable.

Imo a good template.

Are you suggesting the plugins must push two values to the stack? The 20h needed to define an enemy attack is already present, so yes, you need only use the 91 opcode once, push one 0-FFFF attack index to the stack, and change the data in 2070.

For instance, in pseudo code, a plugin to check for allies without haste:

 IF there are allies with Haste = 0 AND MP Cost of attack X < Current MP then:
    Change 2070 to allies without haste
    Pop one value from the stack
    Push the index of haste or the like to the stack
  ELSE go to next plugin (current address + the length of the plugin from this point)


Quote from: secondadvent
//these following three lines are redundant/useless as the above already chose a random target, and the one
//below doesn't even set the target to the new random target (missing 90... did you just c/p and forget to
//delete that part? tongue).

No. I can't quite understand what you're asking me. It might help if I take you through the way the code works. In turn:
- the header flips a coin
- it sets the default attack on the basis of this coin toss
- it then flips another coin
- and sees whether or not to consult the plugins

A default attack must be set first, because the conditions of the plugins might not be met.

So, what the code does is:

12 2070
02 20a0
82
90 Sets target to random opponent
60 20
61 FFFF Pushes data for primary attack to the stack

Sets the normal default attack


81
60 02
34 creates a random number, sees if it divides by 2
70 XXXX skips the next section (which would write new attack data) if it *does* divide by two

50% of the time, refuses to overwrite it

12 2070
02 20A0
82
90
91
61 EEEE erases data on the stack, and adds data for the secondary attack -assuming this piece of code isn't skipped

Otherwise, it resets the target, pops the previous attack digit, and adds its own. I had accidentally deleted the 90, though, so thanks for pointing that up!

81 <---- XXXX points here
60 02
34
70 YYYY

Now we ask whether or not to proceed with plugins.

Flipping two coins takes less space than rolling a four-sided dice (so to speak), pushing the value to an address, then using comparators.

Quote from: secondadvent
the way i have been writing the AI lately has made as much use of the stack as possible to reduce the overall size by a good bit the larger the AI is, but it becomes harder to keep track of, especially since the way i have been coding causes PrC to spit out errors and give up disassembling it tongue.

Yes, using the stack is always preferable to relying on memory addresses. It's also slightly faster from a technical perspective, as the processor doesn't need to wait on RAM, AFAIK.

Quote from: secondadvent
Edit: adding comments within the code may also help it to become clearer, rather than just placing it all and then commenting after, and explains things line-by-line rather than as a whole (you could still keep your summary afterwards, but comments make things easier to understand). not saying that the current layout isn't good as it is, but newcomers to the AI may have more trouble understanding without "holding their hand" through it.

Maybe. The idea is that newbies should just be able to copy paste, though, and add custom values where told to (digits for certain statuses, and the addresses they'll have to derive themselves by adding a specified digit to the current address). Still, I suppose it can't hurt, can it?

Quote from: Covarr
Waitaminnit...

50% chance of a smart attack, 50% chance random?

This is basically assuming that all enemies are the same intelligence level. Wouldn't it make more sense to be able to adjust these odds per enemy? I mean, it seems like Reno would be a hell of a lot more likely to choose an intelligent attack than Hell House.

Indeed - I'd already pre-empted this. Just change the last pushed digit in the header to alter the chances of an intelligent attack, where the chance is 1 / the digit.

You can choose 'odd' intelligence ratios, like 2/3rds, too, but it's a little more complicated.

Frinstance, for

81
60 02
34
70 YYYY

use

81
60 03
34
60 01
42
70 YYYY

What's happening here is that you're pushing a random digit (81) and 3 (60 03) to the stack, then doing a modulo (34). This will pop the two values, and push the remainder - which in this case can be 0, 1, or 2.

I now push 1 to the stack (60 01) and use opcode 42, which pops both and sees if 1 is greater than / equal to the remainder. If it is (2/3 chance), I now have a number 1 sitting on top of the stack. If not, a number 0. The opcode 70 will pop this figure, and skip the plugins if it's a zero. In this case, I have a 2/3 chance of making a 'smart' decision.

So, you can think of

81
60 X
34
60 Y
42
70 YYYY

Where y+1/x is the chance of a 'smart' decision.
« Last Edit: 2010-05-02 01:44:12 by Bosola »

gjoerulv

  • *
  • Posts: 1225
  • me
    • View Profile
    • My Youtube
Welder: A little AI Template Project
« Reply #5 on: 2010-05-03 03:23:56 »
Basically what you need is to make 2 variables (at the very least): 1 for attack and 1 for target. If the condition changes you change the value of the variables. You should then end the code by setting the target and the attack, and finally some sort of check if thee code should loop. A really good ff7 AI should have as few random factors as possible. Just random enough to not make it predictable.

Imo a good template.

Are you suggesting the plugins must push two values to the stack? The 20h needed to define an enemy attack is already present, so yes, you need only use the 91 opcode once, push one 0-FFFF attack index to the stack, and change the data in 2070.

For instance, in pseudo code, a plugin to check for allies without haste:

 IF there are allies with Haste = 0 AND MP Cost of attack X < Current MP then:
    Change 2070 to allies without haste
    Pop one value from the stack
    Push the index of haste or the like to the stack
  ELSE go to next plugin (current address + the length of the plugin from this point)


Sorry, I didn't mean to lecture or anything (if you got that impression)...
It's a good template really, though I would make it slightly different. You could change the footer and header in a way you don't have to worry too much 'bout the stack. some pseudo:
Code: [Select]
//header
while(Condition0)
{
   var tempAttack = XXX; //(defaultAttack)
   var tempTarget = YYY; //(defaultTarget)

   //plugins

   if(condition1)
   {
      temAttack = XXY;
      tempTarget = YYZ;
   }


   if(condition1)
   {
      temAttack = XXZ;
      tempTarget = YYX;
   }

   .
   .
   .

   if(conditionN)
   {
      temAttack = XZZ;
      tempTarget = YXX;
   }

   //footer
   target = tempTarget;
   attack = tempAttack;
   PerformAttack();
}


Edit:

Bytecode:

//header

12 0000
02 20A0
82
90 // Set the tempTarget var (0000) to random opponent
12 0010
61 XXXX
90 // Set tempAttack to default

//plugins
...

//footer
12 2070
02 0000
90 // Target = tempTarget
60 20
02 0010
92 // Perform tempAttack on target.

//Then optionally see if the code should loop (counts as plugin).

... (<- condition)
70 0000

73
« Last Edit: 2010-05-03 03:55:54 by gjoerulv »

Bosola

  • Fire hazard!
  • *
  • Posts: 1752
    • View Profile
    • My YouTube Channel
Welder: A little AI Template Project
« Reply #6 on: 2010-05-03 12:26:38 »
No. I had considered this method, but rejected it. Using the localvars takes up more room: you need to call the localvars and use store opcodes - more verbose than just popping values and pushing anew (as NFITC1 will angrily berate you for ; ) ). As space is limited on the PSOne, saving a few bytes here and there is important.

Keeping the stack clean really isn't that difficult, anyway, especially as there should be no real need for using opcodes that don't pop two vals within each plugin (only 'Jump if not equal' comes to mind).
« Last Edit: 2010-05-03 14:10:37 by Bosola »

nfitc1

  • *
  • Posts: 3011
  • I just don't know what went wrong.
    • View Profile
    • WM/PrC Blog
Welder: A little AI Template Project
« Reply #7 on: 2010-05-03 14:04:12 »
No. I had considered this method, but rejected it. Using the localvars takes up more room: you need to call the localvars and use store opcodes - more verbose than just popping values and pushing anew (as NFITC1 will angrily berate you for ; ) ). As space is limited on the PSOne, saving a few bytes here and there is important.

Very likely, yes. ;) However.....

the way i have been writing the AI lately has made as much use of the stack as possible to reduce the overall size by a good bit the larger the AI is, but it becomes harder to keep track of, especially since the way i have been coding causes PrC to spit out errors and give up disassembling it :-P.

This is a limitation on the way I'm writing PrC. When it's disassembling it is reading it the way the original compiler would have thought it should work. There's no fancy tricks done with it to preserve values on the stack or things. PrC tries to keep track of any values that happen to be on the stack at the time. However it's doing that linearly, ignoring jumps and such. That usually means that there might be things that won't disassemble correctly the would work fine. Take this for example:

Code: [Select]
81
60  01
35
70  0016
12  2070
02  20A0
00  4026
80
60  01
40
82
90
72  XXXX
12  2070
02  20A0
00  4026
80
60  00
40
82
90
72  XXXX
Code: [Select]
If (Random AND 1)
{
TargetMask <- RandomBit( (AllOpponentMask.Flag:BackRow == 1) )
}
Else
{
TargetMask <- RandomBit( (AllOpponentMask.Flag:BackRow == 0) )
}

That gives a 50% chance of picking a target in the backrow vs the frontrow. This will disassemble fine in PrC and is the traditional way for things to happen in FF7 AI.
This does the same thing and will not disassemble fine:

Code: [Select]
12  2070
02  20A0
00  4026
80
81
60  01
35
70  0017
60  00
40
72  001A
60  01
40
82
90
Code: [Select]
TargetMask <- RandomBit( (AllOpponentMask.Flag:BackRow == IIf(RANDOM AND 1 == 0, 0, 1) )

It's several bytes shorter, does the same thing, but will not disassemble correctly in PrC. Look at it linearly and see what the stack is doing:

Code: [Select]
12  2070     Stack: &TargetMask
02  20A0     Stack: &TargetMask, AllActiveEnemies
00  4026     Stack: &TargetMask, AllActiveEnemies, BackRow
80           Stack: &TargetMask, AllActiveEnemies.BackRow
81           Stack: &TargetMask, AllActiveEnemies.BackRow, RANDOM
60  01       Stack: &TargetMask, AllActiveEnemies.BackRow, RANDOM, 1
35           Stack: &TargetMask, AllActiveEnemies.BackRow, (RANDOM AND 1)
70  0017     Stack: &TargetMask, AllActiveEnemies.BackRow
60  00       Stack: &TargetMask, AllActiveEnemies.BackRow, 0
40           Stack: &TargetMask, (AllActiveEnemies.BackRow == 0)
72  001A     Stack: &TargetMask, (AllActiveEnemies.BackRow == 0)
60  01       Stack: &TargetMask, (AllActiveEnemies.BackRow == 0), 1
40           Stack: &TargetMask, ((AllActiveEnemies.BackRow == 0) == 1)
82           Stack: &TargetMask, RandomBit(((AllActiveEnemies.BackRow == 0) == 1))
90           &TargetMask <- RandomBit(((AllActiveEnemies.BackRow == 0) == 1))

PrC's disassembly looks like this:
Code: [Select]
If (Random AND 1)
{

}

}

Else
{
TargetMask <- RandomBit( ( (AllOpponentMask.Flag:BackRow == 0)  == 1) )

Mostly unreadable and completely incorrect. The code doesn't even use any fuzzy logic.

I suppose the moral is, if space isn't an issue then making the code readable should take precedence over making it shorter. If you're good enough to keep track of what you're doing then great! But since you can't make comments in your code it will confuse anyone that might want to use it for something else later if they're not familiar with little tricks like this.
I'm a programmer and writing code that will likely be used by people in the future. That's why I think like this. ;)

Bosola

  • Fire hazard!
  • *
  • Posts: 1752
    • View Profile
    • My YouTube Channel
Welder: A little AI Template Project
« Reply #8 on: 2010-05-03 14:27:15 »
This is a limitation on the way I'm writing PrC. When it's disassembling it is reading it the way the original compiler would have thought it should work. There's no fancy tricks done with it to preserve values on the stack or things. PrC tries to keep track of any values that happen to be on the stack at the time. However it's doing that linearly, ignoring jumps and such. That usually means that there might be things that won't disassemble correctly the would work fine. Take this for example:

Code: [Select]
81
60  01
35
70  0016
12  2070
02  20A0
00  4026
80
60  01
40
82
90
72  XXXX
12  2070
02  20A0
00  4026
80
60  00
40
82
90
72  XXXX
Code: [Select]
If (Random AND 1)
{
TargetMask <- RandomBit( (AllOpponentMask.Flag:BackRow == 1) )
}
Else
{
TargetMask <- RandomBit( (AllOpponentMask.Flag:BackRow == 0) )
}

That gives a 50% chance of picking a target in the backrow vs the frontrow. This will disassemble fine in PrC and is the traditional way for things to happen in FF7 AI.
This does the same thing and will not disassemble fine:

Code: [Select]
12  2070
02  20A0
00  4026
80
81
60  01
35
70  0017
60  00
40
72  001A
60  01
40
82
90
Code: [Select]
TargetMask <- RandomBit( (AllOpponentMask.Flag:BackRow == IIf(RANDOM AND 1 == 0, 0, 1) )

It's several bytes shorter, does the same thing, but will not disassemble correctly in PrC. Look at it linearly and see what the stack is doing:

Code: [Select]
12  2070     Stack: &TargetMask
02  20A0     Stack: &TargetMask, AllActiveEnemies
00  4026     Stack: &TargetMask, AllActiveEnemies, BackRow
80           Stack: &TargetMask, AllActiveEnemies.BackRow
81           Stack: &TargetMask, AllActiveEnemies.BackRow, RANDOM
60  01       Stack: &TargetMask, AllActiveEnemies.BackRow, RANDOM, 1
35           Stack: &TargetMask, AllActiveEnemies.BackRow, (RANDOM AND 1)
70  0017     Stack: &TargetMask, AllActiveEnemies.BackRow
60  00       Stack: &TargetMask, AllActiveEnemies.BackRow, 0
40           Stack: &TargetMask, (AllActiveEnemies.BackRow == 0)
72  001A     Stack: &TargetMask, (AllActiveEnemies.BackRow == 0)
60  01       Stack: &TargetMask, (AllActiveEnemies.BackRow == 0), 1
40           Stack: &TargetMask, ((AllActiveEnemies.BackRow == 0) == 1)
82           Stack: &TargetMask, RandomBit(((AllActiveEnemies.BackRow == 0) == 1))
90           &TargetMask <- RandomBit(((AllActiveEnemies.BackRow == 0) == 1))

PrC's disassembly looks like this:
Code: [Select]
If (Random AND 1)
{

}

}

Else
{
TargetMask <- RandomBit( ( (AllOpponentMask.Flag:BackRow == 0)  == 1) )

Mostly unreadable and completely incorrect. The code doesn't even use any fuzzy logic.

I suppose the moral is, if space isn't an issue then making the code readable should take precedence over making it shorter. If you're good enough to keep track of what you're doing then great! But since you can't make comments in your code it will confuse anyone that might want to use it for something else later if they're not familiar with little tricks like this.
I'm a programmer and writing code that will likely be used by people in the future. That's why I think like this. ;)

In this case, though, I think space really is an issue, unless we find out more about haxxoring the SCUS_blahblahblah filetable (I think Gemini has some info on this, which I'd like to see more of). Whilst it's true that readability is nice, the real purpose is to open up easy AI editing to those who wouldn't be interested in learning assembler etc. anyway.

At any rate, authors are quite free to write 'plugins' however they wish - internally handling localvars and what have you is absolutely fine, you just need to remember to end the plugin with the right values on the stack. Hell, you could even do it with a pen and paper - write a list of things on the stack, crossing them out as they get popped. Some writers might not create very efficient plugins, but users can choose between competing plugins, that offer different lengths, features, and customization. And hey, it's a forum, they're free to ask if they so choose.

Oh, and I'd forgot about the 'Random, push X, bitwise AND' trick. Nice.

What are your thoughts on Welder otherwise, though?

nfitc1

  • *
  • Posts: 3011
  • I just don't know what went wrong.
    • View Profile
    • WM/PrC Blog
Welder: A little AI Template Project
« Reply #9 on: 2010-05-03 15:37:49 »
In this case, though, I think space really is an issue, unless we find out more about haxxoring the SCUS_blahblahblah filetable (I think Gemini has some info on this, which I'd like to see more of). Whilst it's true that readability is nice, the real purpose is to open up easy AI editing to those who wouldn't be interested in learning assembler etc. anyway.

True enough. There are other ways to decrease size besides making AI code a few bytes shorter. Deleting entire scenes works well because 7808 bytes of the same value compress into about 16 bytes or so. :)

At any rate, authors are quite free to write 'plugins' however they wish - internally handling localvars and what have you is absolutely fine, you just need to remember to end the plugin with the right values on the stack. Hell, you could even do it with a pen and paper - write a list of things on the stack, crossing them out as they get popped. Some writers might not create very efficient plugins, but users can choose between competing plugins, that offer different lengths, features, and customization. And hey, it's a forum, they're free to ask if they so choose.

Oh, and I'd forgot about the 'Random, push X, bitwise AND' trick. Nice.

What are your thoughts on Welder otherwise, though?

The idea is great, but you'll need to change your header slightly. It needs to pop the values of the header's base attack

Code: [Select]
12 2070
02 20a0
82
90
60 20
61 index of the 'dumb' attack

81
60 X
34
60 Y
42
70 ZZZZ
91
91   <if probability passes to execute "intelligent attack", remove the pushed attack/command indexes>

Also, consider the following "plugin":

<header>      Default attack are set here so there are two values on the stack
<given a probablility, jump to footer>
81
60  17
34
60  00
71  XXXX
<set target and push attacks>
<jump to footer>
<more options based on value of (Random MOD 23)>
...
<footer>      Now there are three values on the stack!

71 doesn't pop both values it compares so there would still be a value at the back of the stack. When you'd get to the end of the footer you'd still have the (Random MOD 23) on the bottom of the stack. I don't know what consequences this has. All the scripts are written in the way that the stack is empty when the 73 is "executed". I believe this a safe practice as I'm not sure if a new stack is created each time a script runs or if there's one stack at a time. So I say there should be a reserved intermediate "need pop" localvar (say LocalVar:1000) that indicates if there needs to be a pop at the end.

10  1000
60  01
90      These values are popped so essentially clean stack still. Not part of the header since 71 might not be used in plugin
81
60  17
34
60  00
71  XXXX
<set target and push attacks>
<jump to footer>
<more options based on value of (Random MOD 23)>
...

Then the footer should be:

Code: [Select]
92
00  1000
70  XXXX (skip the next byte to the ending)
91
73

Since PrC updates jumps (you're welcome ;) ) then the footer doesn't have to be rewritten. This ensures that the stack is empty by the end and the pushed attack data isn't lost.

Also, the RANDOM AND XXX would only work with powers of 2. Since RANDOM creates a Word, then it would give chances from 1:2^-1 to 1:2^-16 in powers of 2. Some things could be pretty rare, but if that's what you're going for then great. :D

Bosola

  • Fire hazard!
  • *
  • Posts: 1752
    • View Profile
    • My YouTube Channel
Welder: A little AI Template Project
« Reply #10 on: 2010-05-03 15:48:09 »
True enough. There are other ways to decrease size besides making AI code a few bytes shorter. Deleting entire scenes works well because 7808 bytes of the same value compress into about 16 bytes or so. :)

Too true.

The idea is great, but you'll need to change your header slightly. It needs to pop the values of the header's base attack

No, I can see why you think that, but you've misunderstood - plugins can alter the attacks, but don't have to - they might keep the default attack. Consider the following

Default attack: Hit random with hammer
1/2 chance of plugins:
 - if enemy is weakened, finish them off
 - if in critical, change 'hit with hammer' to 'cast cure on self'

Here, the monster (let's call him Alan) will by default hit things with hammers. Sometimes, he'll make smarter decisions - but there are still circumstances where hitting a random enemy with a hammer is the 'smart' choice for Alan to make. The default choice remains in these circumstances. Plus, plugin authors and users don't need to worry about always maintaining a 'plan B' attack index at the end of each plugin.

tl;dr - Plugins check for conditions, but you don't *have* to have any certain to come 'true'.

Quote
Also, the RANDOM AND XXX would only work with powers of 2. Since RANDOM creates a Word, then it would give chances from 1:2^-1 to 1:2^-16 in powers of 2. Some things could be pretty rare, but if that's what you're going for then great.

Yes. I already understood that.  :wink:
« Last Edit: 2010-05-03 15:54:34 by Bosola »

nfitc1

  • *
  • Posts: 3011
  • I just don't know what went wrong.
    • View Profile
    • WM/PrC Blog
Welder: A little AI Template Project
« Reply #11 on: 2010-05-03 16:02:38 »
No, I can see why you think that, but you've misunderstood - plugins can alter the attacks, but don't have to - they might keep the default attack. Consider the following

Default attack: Hit random with hammer
1/2 chance of plugins:
 - if enemy is weakened, finish them off
 - if in critical, change 'hit with hammer' to 'cast cure on self'

Here, the monster (let's call him Alan) will by default hit things with hammers. Sometimes, he'll make smarter decisions - but there are still circumstances where hitting a random enemy with a hammer is the 'smart' choice for Alan to make. The default choice remains. Plus, plugin authors and users don't need to worry about always maintaining a 'plan B' attack index at the end of each plugin.

tl;dr - Plugins check for conditions, but you don't *have* to have any certain to come 'true'.

The pops don't need to be in the header, but they do need to be somewhere before changing the attack or else the stack wouldn't be empty by the end of the script. I believe it's safer to empty the stack since I'm not quite sure what happens at the end. Does the stack get cleared by the 73 or does it get reinitialized at the beginning of a new script? I'm not sure so I'm going to be more cautious with it. All the scripts execute and leave a clean stack in their wake. Otherwise you'll have some leftovers when scripts get run again. So I guess that falls upon the plugin author to account for, doesn't it?

One more thing; The header should be changed to:

Code: [Select]
12 2070
02 20a0
82
90
60 20
61 index of the 'dumb' attack

81
01  1020
34
01  1040
42
70 ZZZZ

This way, the probabilities can be changed mid-battle based on counter attacks and such and be initialized in the Pre-Battle scripts.

11  1040
60  3
90
11  1020
60  7
90              Roughly 42% chance of doing something "smart"

That's personal preference of course. If you don't have any desire to change probabilities then you could leave them as static values.

gjoerulv

  • *
  • Posts: 1225
  • me
    • View Profile
    • My Youtube
Welder: A little AI Template Project
« Reply #12 on: 2010-05-03 16:11:44 »
No. I had considered this method, but rejected it. Using the localvars takes up more room: you need to call the localvars and use store opcodes - more verbose than just popping values and pushing anew (as NFITC1 will angrily berate you for ; ) ). As space is limited on the PSOne, saving a few bytes here and there is important.

Keeping the stack clean really isn't that difficult, anyway, especially as there should be no real need for using opcodes that don't pop two vals within each plugin (only 'Jump if not equal' comes to mind).

It's takes (not really that much) more space, yes, but imo it looks more sexier if you do it this way. For a programmer, easy readable code is important. But I'm fully aware the PS1 has space issues, and taken the space problem into consideration, people would probably benefit more from your method. I'm just saying how I would've done it.

Personally, I wouldn't use this template simply 'cause it's limited. That doesn't mean it's not a good template to work with. It would be fun to make some plugins.

...
I suppose the moral is, if space isn't an issue then making the code readable should take precedence over making it shorter. If you're good enough to keep track of what you're doing then great! But since you can't make comments in your code it will confuse anyone that might want to use it for something else later if they're not familiar with little tricks like this.
I'm a programmer and writing code that will likely be used by people in the future. That's why I think like this. ;)

Yup, that's a good morale. I'm a programmer myself, and some of my algorithms are out there in the world lol. And one of the things I'm most concerned with is that people easily understands the code. But I must admit I'm a bit lazy when it comes to progs I have no intention releasing a source on. :P

Bosola

  • Fire hazard!
  • *
  • Posts: 1752
    • View Profile
    • My YouTube Channel
Welder: A little AI Template Project
« Reply #13 on: 2010-05-03 16:15:40 »
Quote
The pops don't need to be in the header, but they do need to be somewhere before changing the attack or else the stack wouldn't be empty by the end of the script.

Yes. In the first draft of the opening post, I made that a lot clearer, saying 'you need to leave two values on the stack, one being the 20, the other being a valid attack index'. The idea I proposed involved clearing the stack in the plugins. I think that having each plugin use a single 91 opcode will be shorter overall than popping the values in the header and lengthening a plugin such that an attack will *always* be performed in any circumstance.

Quote
Does the stack get cleared by the 73 or does it get reinitialized at the beginning of a new script?

I believe not. See a thread I posted on GameFAQs here: http://www.gamefaqs.com/boards/genmessage.php?board=197341&topic=52751515

Quote
One more thing; The header should be changed to:

Code:
12 2070
02 20a0
82
90
60 20
61 index of the 'dumb' attack

81
01  1020
34
01  1040
42
70 ZZZZ

This way, the probabilities can be changed mid-battle based on counter attacks and such and be initialized in the Pre-Battle scripts.

11  1040
60  3
90
11  1020
60  7
90              Roughly 42% chance of doing something "smart"

That's personal preference of course. If you don't have any desire to change probabilities then you could leave them as static values.

That's a pretty cool idea - enemies that get smarter (or dumber) under certain conditions.

I can already imagine an application: an enemy and his pets. The pets become less intelligent on the death of their master, using less pointed, and maybe less 'efficient' (ie less powerful) attacks. All it would take is a change to var 1020 / 1040 on the 'master's' death.

Bosola

  • Fire hazard!
  • *
  • Posts: 1752
    • View Profile
    • My YouTube Channel
Welder: A little AI Template Project
« Reply #14 on: 2010-05-03 16:25:59 »
It's takes (not really that much) more space, yes, but imo it looks more sexier if you do it this way. For a programmer, easy readable code is important.

On a tangent, but I have to object to this, with respect.

Yes, readability is important when space and speed aren't issues, but it's not true to claim all programmers always favour this sort of philosophy. Granted, using less-than-readable code, compiler exploits for space, etc. etc. often gets derided as 'cowboy coding', but different coding strategies suit different circumstances IMHO.

Consider Micro-Soft's famous BASIC interpreter (one of their first commercial applications - actually written by Gates!) - they actually have the application jump to the mid-way part of a three-byte instruction (so it starts reading from the second byte). This data is actually intelligible and works as intended - that's pretty tight coding! Why go to all this trouble? To make a BASIC interpreter that actually ran and left headspace on the Altair Personal Computer - giving MS a massive commercial advantage. Of course, for modern organizations, a few bytes give them virtually no commercial advantage - being able to reduce costs in making code 'portable', on the other hand...

It all matters on the circumstance. The idea for me is to produce a header that doesn't really need to be read, but does need to be fairly compact, because space is still an issue with the PSX (YAMADA.BIN alterations not withstanding).

Quote
Personally, I wouldn't use this template simply 'cause it's limited. That doesn't mean it's not a good template to work with. It would be fun to make some plugins.

Hey, that's fine. There are quite a few enemies this wouldn't really fit anyway, like most bosses.

gjoerulv

  • *
  • Posts: 1225
  • me
    • View Profile
    • My Youtube
Welder: A little AI Template Project
« Reply #15 on: 2010-05-03 16:56:22 »
It's takes (not really that much) more space, yes, but imo it looks more sexier if you do it this way. For a programmer, easy readable code is important.

On a tangent, but I have to object to this, with respect.

Yes, readability is important when space and speed aren't issues, but it's not true to claim all programmers always favour this sort of philosophy. Granted, using less-than-readable code, compiler exploits for space, etc. etc. often gets derided as 'cowboy coding', but different coding strategies suit different circumstances IMHO.
...

Well, yeah, I agree. That's why I said people would probably benefit more from your method when it comes to space.

nfitc1

  • *
  • Posts: 3011
  • I just don't know what went wrong.
    • View Profile
    • WM/PrC Blog
Welder: A little AI Template Project
« Reply #16 on: 2010-05-03 17:26:33 »
Quote
The pops don't need to be in the header, but they do need to be somewhere before changing the attack or else the stack wouldn't be empty by the end of the script.

Yes. In the first draft of the opening post, I made that a lot clearer, saying 'you need to leave two values on the stack, one being the 20, the other being a valid attack index'. The idea I proposed involved clearing the stack in the plugins. I think that having each plugin use a single 91 opcode will be shorter overall than popping the values in the header and lengthening a plugin such that an attack will *always* be performed in any circumstance.

Oh, sorry. I must have glanced over this. :) Good point.

Quote
Does the stack get cleared by the 73 or does it get reinitialized at the beginning of a new script?

I believe not. See a thread I posted on GameFAQs here: http://www.gamefaqs.com/boards/genmessage.php?board=197341&topic=52751515

Should be easy enough to test, actually. Find some enemy that has a basic attack and see if you can get it to perform that attack based on stack leftovers. I'd suggest using the Kalm Fang. It's got probably the simplest AI of all of them. Re write it to do this:

Pre-Battle
60  20
61  0179 (I think this is its bodyblow)

Main
12  2070
02  20A0
80
82
90
92
60  20
61  0179    (for next round)
73

If that doesn't crash the game then the stack doesn't reset between script calls.

Armorvil

  • *
  • Posts: 621
  • Working on : FFVII Total Grudge
    • View Profile
Welder: A little AI Template Project
« Reply #17 on: 2010-05-04 19:11:14 »
Coming from someone without programming notions, this is indeed a great project. To be quite honest, I'm still struggling to understand all those push, stack and pop talk :P

In the past, I used FF3usME quite a lot (multi-editor for FFVI), and I'd love that one day, editing AI for FFVII would be as simple. Maybe these templates and plugins could be implemented to Proud Clod, somehow ? Like, by clicking on boxes, in the AI editor, which would lead to a list of the monster's abilities... ...Selecting one would add the AI script so the enemy casts the selected spell. Then, you click the next box to choose the chance for it to cast it, and the conditions. Another box would be to decide the target, etc...

Actually, that sounds like too much work for NFITC1 :P
Never mind about this message  :-X

Bosola

  • Fire hazard!
  • *
  • Posts: 1752
    • View Profile
    • My YouTube Channel
Welder: A little AI Template Project
« Reply #18 on: 2010-05-05 19:30:50 »
To be honest, you could probably do it with just an excel spreadsheet and a macro.

nfitc1

  • *
  • Posts: 3011
  • I just don't know what went wrong.
    • View Profile
    • WM/PrC Blog
Welder: A little AI Template Project
« Reply #19 on: 2010-05-05 21:11:24 »
Coming from someone without programming notions, this is indeed a great project. To be quite honest, I'm still struggling to understand all those push, stack and pop talk :P

In the past, I used FF3usME quite a lot (multi-editor for FFVI), and I'd love that one day, editing AI for FFVII would be as simple. Maybe these templates and plugins could be implemented to Proud Clod, somehow ? Like, by clicking on boxes, in the AI editor, which would lead to a list of the monster's abilities... ...Selecting one would add the AI script so the enemy casts the selected spell. Then, you click the next box to choose the chance for it to cast it, and the conditions. Another box would be to decide the target, etc...

Actually, that sounds like too much work for NFITC1 :P
Never mind about this message  :-X

None of that is actually hard. I eventually plan to allow for some simple templates like that, but the truth is that writing AI for FFVII is hundreds times more difficult than it is for FFVI. Yes, I used FF3usME back when it was still being developed and that's part of what made me want to make PrC do what it does. For FFVI though it's a lot more simplistic. "Pick random target" is only one or two bytes where doing it in FFVII takes no fewer than 9. Attacks are also more complicated, blah, blah, blah, etc.

I guess there could be a menu where it inserts certain things like "random target" or "perform attack (x)". For now unfortunately, it'll stay the way it is. Sorry. :(

Idec Sdawkminn

  • *
  • Posts: 47
  • -im the real idec (REALLY)-
    • View Profile
    • My Facebook
Welder: A little AI Template Project
« Reply #20 on: 2010-05-06 23:41:12 »
From what I can understand, this sounds good.

Not entirely on topic, but I was thinking... Materia is readily available to anyone who goes to a town. It would be cool to have enemies, maybe just humanoid enemies, have some materia combinations. Some even Final Attack+Revive or something. You could have plugins for materia combinations, most just using the ones from the town they are near, and have it randomly assign those to the enemies at the start of the battle. The same enemy can have different attacks. Each materia plugin could have a code, like All+Cure will be assigned the restorative code, or Fire would be given the magic attack code.

Bosola

  • Fire hazard!
  • *
  • Posts: 1752
    • View Profile
    • My YouTube Channel
Welder: A little AI Template Project
« Reply #21 on: 2010-05-08 16:19:51 »
This plugin heals allies when they're in low health:

Code: [Select]
02 2060
02 4140
80
61 <healing spell index>
86
42
70 <address of next plugin - should be current + 31h, but do check>
12 1000
02 2080
03 4160
80
02 2080
03 4180
80
60 Y
33
60 X
32
43
90
02 1000
60 1
42
70 <address of next plugin - should be current + Fh, but do check>
12 2070
02 1000
82
90
91
61 <healing spell index>

Replace 'healing spell index' with the attack index of Cure, Cure2, etc, and put in the address of the next plugin where told to (if no other plugins come afterwards, you'd put the address of the footer, obviously). X/Y is the fraction of HP a creature must be at or below to be considered for healing. If several creatures meet this requirement, a random one will be selected. This plugin checks for MP, and will not try and cast a spell beyond its MP pool.

I have tested this, and it seems to work. I would appreciate any feedback from other testers, though.

Specifically, I tried it on a Nerosuferoth, turning heatwing into a curative 'spell'. The code is just the header and plugin:

Code: [Select]
12 2070
02 20A0
82
90
60 20
61 01C5 <- Default attack, which I named 'Standard'
81
60 04
34
60 02 <- 'IQ' is 3/4
42
70 0054 <- Goes to footer
***** THE FOOTER HAS ENDED *****
***** NOW THE PLUGIN STARTS *****
02 2060
02 2140
80
61 01C6 <- The index of the healing attack (which I named 'Healwing'), checks MP requirements
86
42
70 0054 <- Goes to next plugin, or in this case, the footer (because I'm not using any other plugins)
13 1000 <- I'm using this to hold lists of creatures with HP below X/Y
02 2080
03 4160
80
02 2080
03 4180
80
60 03
33
60 02 <- In this case, X/Y = 2/3rds, so a creature with 66% or less HP will qualify
32
43
90
03 1000
60 01 <- Checks to see if any such creatures actually meet these requirements
42
70 0054 <- Goes to next plugin, or in this case, the footer (because I'm not using any other plugins)
12 2070
03 1000
82
90
91
61 01C6 <- We've just popped the default attack, and we're now pushing the 'healwing' index
***** THE PLUGIN ENDS HERE *****
< The next plugin would start here - if the above was not the last. >
< In this case, there are no other plugins to include, so we proceed directly to the footer >
***** THE FOOTER: *****
92 <- This is the footer, or opcode 92. In this case, it's at address 54h.

Quote
You could have plugins for materia combinations, most just using the ones from the town they are near, and have it randomly assign those to the enemies at the start of the battle. The same enemy can have different attacks. Each materia plugin could have a code, like All+Cure will be assigned the restorative code, or Fire would be given the magic attack code.

This would be sort of doable, yes. You'd need to put random values into certain variables at the battle start, and then create plugins that only 'activate' when those variables say so. The AI would end up quite bulky this way, however.

EDIT: Addendum

You should be able to modify this plugin to target only particular creatures. This is useful when one species 'looks after' another, or when a creature's friend absorbs an elemental attack (and can thus be healed with non-curative spell). You would do it like

Code: [Select]
12 1000
02 2080
02 4120
80
60 <species index - see indeces listed in formation>
40
90
02 2060
02 4140
80
61 <heal/ elemental attack index>
86
42
70 <next plugin>
12 1000
02 1000
03 4160
80
02 2080
03 4180
80
60 Y
33
60 X
32
43
90
02 1000
60 1
42
70 <next plugin>
12 2070
02 1000
82
90
91
61 <heal/ elemental attack index>

All this does is make a list of allies with ID X and store it in localvar1000. It will then consult with these to check for HP / target critical allies. As a rule, if using both plugins, you should insert this one second to give it higher priority.

Of course, you could always achieve this by creating a list of creatures of species X in the pre-battle script, but that might complicate things too much for the target audience (people who want to at most cut and paste, and maybe tweak the odd variable).

EDIT 2: Another Plugin, this time using attack X on random opp with status Y

If correctly written (and I've leave others to say so or not), this will be an important plugin - useful for not only buffing and debuffing, but things like "Take out opps. in critical", etc.

Here's the essential template

Code: [Select]
OPTIONAL : CHECK MP

02 2060
02 4140
80
61 <index of spell / attack>
86
42
70 <address of next plugin - MP fail>

MAIN: IS OPP IN STATUS --?

13 1000
02 20a0 <2080 for allies>
00 <status flag address in memory - 4001 for Critical>
80
90
<this has just created a list of enemies and their critical statuses>
03 1000
60 01
42 <make 45 to look for enemies WITHOUT the status>
70 <address in AI of next plugin / footer>
12 2070
03 1000
82
90
91
61 <index of attack>

So, the monster can attack a low-HP party member to KO them.

This could have several other implementations. Frinstance, a simple debuff plugin:

Code: [Select]
02 2060
02 4140
80
61 [index of Silence]
86
42
70 <address of next plugin - MP fail>
13 1000
02 20a0
00 400a <stop>
80
90
<this has just created a list of enemies and their stop statuses>
03 1000
60 01
45 <not stop>
70 <address in AI of next plugin / footer>
12 2070
03 1000
82
90
91
61 <index of silence>

The only issue with this is that it would keep using debuff spells on party members immune to them, unless we were using the default attack (chance depends on IQ) / higher-priority events came up. Most battles don't last all that long, though, so it wouldn't be that noticeable. You could get around it by adding a 'if Mod of Random and X > Y, go and do something else' piece to the start of the plugin.

More interesting are implementations like

Code: [Select]
02 2060
02 4140
80
61 <index of deathblow like attack>
86
42
70 <address of next plugin - MP fail>
13 1000
02 20a0
00 400a <stop>
80
90
<this has just created a list of enemies and their stop statuses>
03 1000
60 01
42
70 <address in AI of next plugin / footer>
12 2070
03 1000
82
90
91
61 <index of deathblow-like attack, that obviously cannot miss an enemy in stop>

With the two variants on this plugin, you've now got a monster who casts stop and then exploits your lack of evasion to rain down a powerful but inaccurate attack.
Likewise with casting only magic on sleeping party members.

MULTIEDIT: BOARD SOFTWARE IS BORKED, WILL NOT LET ME DELETE CODE TAGS FROM POAST. RECTIFY PLZ.
« Last Edit: 2010-05-09 10:52:06 by Bosola »

nfitc1

  • *
  • Posts: 3011
  • I just don't know what went wrong.
    • View Profile
    • WM/PrC Blog
Welder: A little AI Template Project
« Reply #22 on: 2010-05-09 01:53:10 »
MULTIEDIT: BOARD SOFTWARE IS BORKED, WILL NOT LET ME DELETE CODE TAGS FROM POAST. RECTIFY PLZ.

You've got two [cod e] tags in your first code block. Get rid of those and you can get rid of those ending terminators.

Bosola

  • Fire hazard!
  • *
  • Posts: 1752
    • View Profile
    • My YouTube Channel
Welder: A little AI Template Project
« Reply #23 on: 2010-05-09 10:51:01 »
Aha, never saw those. Guess I should re-edit that to

Quote
BOARD SOFTWARE IS NOT BORKED, MY GLASSES ARE

Anyways, here's an idea of the plugins I think need to be sorted out:

- header / footer (done)
- heal /hurt ally / opp with less/more than X/Y HP (done)
- cast X on ally / opp with status X (done)
- cast X on ally/opp with status X AND Y
- cast X on ally/opp with status X OR Y
- cast X on ally/opp with status X AND NOT Y
- cast X on self when opps have status X [eg reflect]
- cast X when in status X
- cast X ally / opp with greatest / least STAT (Magic, Dexterity, Strength, Vitality, HP, MP - whatever)
- cast X on random when a LOCALVAR-a is at 1
- cast X on LOCALVAR-a when LOCALVAR-b is at 1
- random chance of other attack
- escape when all allies inc. self dead OR critical
- cast X when below/above X/Y HP [kamikaze, darkside, etc]

I think with those the vast majority of operations will be covered. Can you think up any more?

« Last Edit: 2010-05-09 11:06:11 by Bosola »

nfitc1

  • *
  • Posts: 3011
  • I just don't know what went wrong.
    • View Profile
    • WM/PrC Blog
Welder: A little AI Template Project
« Reply #24 on: 2010-05-10 16:38:50 »

- cast X on ally/opp with status X AND Y
- cast X on ally/opp with status X OR Y
- cast X on ally/opp with status X AND NOT Y
- cast X on self when opps have status X [eg reflect]
- cast X when in status X
- cast X ally / opp with greatest / least STAT (Magic, Dexterity, Strength, Vitality, HP, MP - whatever)
- cast X on random when a LOCALVAR-a is at 1
- cast X on LOCALVAR-a when LOCALVAR-b is at 1
- random chance of other attack
- escape when all allies inc. self dead OR critical
- cast X when below/above X/Y HP [kamikaze, darkside, etc]

Very easy. Some enemies do these kinds of things.

- cast X on ally/opp with status X AND/OR/AND NOT Y
Code: [Select]

Most of the other cases you listed are just as easy. Check out Eligor's Main for some ideas.
12  2070
02  20A0  <- enemies, change to 20B0 for allies
00  40XX  <- change XX to status1
80
60  01    <- or 00 for NOT having status
40
02  20A0
00  40XX  <- status2
80
60  01
40
35        <- this is status1 AND status2, 36 is (status1 OR status2), 37 before the 35 is (status1 AND NOT status2)
82        <- random target that fits the above restrictions (optional)
90        <- store target
60  20
61  YYYY  <- attack
92

Translates:
TargetMask <- RandomBit( (AllOpponentMask.Status:status1 == 1)  AND  (AllOpponentMask.Status:status2 == 1) )