Chapter 5: Peeking inside the AI
The AI system in Skool Daze is very complex, as you surely imagined watching all those characters moving around the school with their own agenda and the many different situations that may occur; this is what gives the player this feeling of freedom which made this game famous in the first place.
In order to keep the same feeling and being close to the original, I have studied and tried to imitate the AI system in the original game. The “fine grain” details are quite complex as well as the implementation, full of tricks with the stack and many many entry points to each routine, for instance.
But the general schema is easier to explain, and that is what I would try to do here. The AI system consists of four different types of commands:
The Primary Command
The Primary Command states what the character must do from the top level of abstraction, for instance make a character go to a specific location, or flag a given event or find Eric.
At each lesson (time periods in which the game is divided), a command list is specified for each character (command_list_high/low fields in data.s). This command list is a script with primary commands the character has to perform in this period (if he is able to – when the lesson finishes, a new command list is loaded and started).
This is an example of the command list for a teacher who should teach in the Reading Room this lesson (extracted from data.s):
Code: Select all
.byt SC_GOTO, D_LIBRARY_INT ; Go to the library
.byt SC_GOTO, D_READING_DOORWAY ; Go to the entrance of the room
.byt SC_RESTIFNOLESSON ; Restart until it is time to start the lesson
.byt SC_FLAGEVENT, E_TEACHER_READING ; Signal that the teacher has arrived
.byt SC_MSGSITDOWN ; Tell the kids to sit down
.byt SC_GOTO, D_POS_READING1 ; Go to the teacher position
.byt SC_GOTO, D_POS_READING2 ; Go to the teacher position 2
.byt SC_DOCLASS ; Wipe the board & conduct the class
And this one controls a little boy when he has class in the exam room:
Code: Select all
.byt SC_GOTO, D_EXAM_ROOM ; Go to the exam room
.byt SC_MOVEUNTIL, E_TEACHER_EXAM ; Wait until teacher arrives
.byt SC_FINDSEAT ; Find a seat and sit down
.byt SC_END ; Sit still
The routines associated with every command can be found in script.s (s_*). Usually their name matches the token’s name quite well. The table with routine addresses is stored in data.s
Whenever a command list finishes, the high byte of the pointer is set to zero.
Interruptible and Uninterruptible Subcommands
But the behavior of the character is not so simple. While executing any Primary Command in their command list, the character might be set to perform a Subcommand. Most times this is because the most complex Primary Commands use Subcommands in their implementation, but also for other reasons. There are two types of subcommands: those which can be interrupted by another action and those which cannot.
Examples of Interruptible Subcommands are guide a character up or down a staircase or make a character speak. The character data space contains a pointer to the interruptible Subcommand routine which is being executed in i_subcom_low/high. If the high byte is zero, no interruptible subcommand is present. Implementations of those routines can be found in script.s (s_isc_* routines).
A final word on why these are interruptible. Imagine Einstein is grassing on you in class. You can punch him or hit him with your catapult and he will fall down (nice!). When he gets up again and sits down, he will continue where he was interrupted, though.
Examples of Uninterruptible Subcommands are those who control the travel of a catapult pellet, or those who make Angelface throw a punch, or those dealing with characters that have been knocked down. These routines are named s_usc_* in script.s and, of course, each character data space contains a pointer to the current one uni_subcom_high/low, where the high byte is zero if no subcommand is present.
Most of the routines check if the character has an uninterruptible subcommand in execution and avoid interrupting it.
Continual Subcommands
Yes, there is yet another class of subcommands, but I think they are better explained in a different section. The Continual Subcommands are a second class of interruptible subcommands which run in parallel with them (sort of). The best way to explain why they are there is with a clear example: the one which checks if Angelface is touching Eric (when the former has mumps) or the one which makes Angelface hit other kids now and then. Such a Continual Subcommand is placed in cont_subcom_low/high in the character data space by the Primary Command SC_SETCONTSUB, and they are executed while the character is doing other tasks. Implementations in script.s (csc_* routines).
Take a look at the command list associated to Angelface when he has to go to the map room in this lesson:
Code: Select all
.byt SC_SETCONTSUB, CS_HITNOWTHEN ; Put a command making Angelface hit now & then
.byt SC_GOTO, D_MAP_ROOM ; Go to the reading room
.byt SC_SETCONTSUB, CS_HITNOWTHEN ; Put a command making Angelface hit now & then
.byt SC_MOVEUNTIL, E_TEACHER_MAP ; Move about until the the teacher arrives
.byt SC_FINDSEAT ; Find a seat and sit down
.byt SC_END ; Sit still
In Skool Daze the main loop takes the following steps:
1/ If there is an uninterruptible subcommand routine address, jump (jmp) to it
2/ If there is a continual subcommand routine call it (using a jsr so control then gets to step 3)
3/ If there is an interruptible subcommand, jump to it
4/ Restart the command list if a flag in the character’s data indicates so
5/ If there is a primary command routine, jump to it
6/ Remove the continual subcommand if present (we have finished the primary command here, so the associated continual subcommand must be finished too)
7/ Collect the next primary command routine address from the command list, and jump to it
With some slight variations, this is the way it is done in the speccy version of the game.
If you check engine.s, this main loop is:
Code: Select all
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
; This routine mimics the Speccy version, which apparently
; checks 3 characters in case they need moving at each
; frame. Seems too slow on Oric?
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
move_chars
.(
; We will check 7 characters
lda #6
sta tmp6
loop
ldx last_char_moved
jsr move_char
dec tmp6
bne loop
ldx last_char_moved
+move_char
cpx #20
bne notend
; if in demo mode (set by Self-modifying code)
+demo_ff_00
ldx #0 ; Don't move Eric here
notend
inx
stx last_char_moved
Up to here pretty straightforward, except for the self-modifying code used to switch between demo and play. Now we need to check if the character must be moved (they have different speeds and such things). This is quite tricky because it uses a jsr which returns here only if the character must move and gets rid of the context and returns to the previous caller if not (issuing a pla:pla:rts).
Code: Select all
; Reg X contains the character ID. This is usually kept
; unchanged. The next jsr is a bit tricky as it checks if
; a character must move and, if not, does pla:pla:rts
jsr must_move
Time to move a character then, perform the 7 steps specified above. Quite easy to follow:
Code: Select all
; If we arrive here the character must be moved, then we take the 7 steps.
+unint_subcommand
;; Step 1
lda uni_subcom_high,x
beq nounisubcommand
sta tmp0+1
lda uni_subcom_low,x
sta tmp0
jmp (tmp0)
nounisubcommand
+cont_subcommand
;; Step 2
lda cont_subcom_high,x
beq nocontsubcommand
sta smc_cont_jump+2
lda cont_subcom_low,x
sta smc_cont_jump+1
smc_cont_jump
jsr $dead ; this is a jsr, not a jmp as the rest...
nocontsubcommand
+int_subcommand
;; Step 3
lda i_subcom_high,x
beq noisubcommand
sta tmp0+1
lda i_subcom_low,x
sta tmp0
jmp (tmp0)
noisubcommand
+check_reset_cl
;; Step 4
lda flags,x
and #RESET_COMMAND_LIST
beq no_reset
lda flags,x
and #(RESET_COMMAND_LIST^$ff)
sta flags,x
lda #0
sta cur_command_high,x
sta pcommand,x
no_reset
+primary_command
;; Step 5
lda cur_command_high,x
beq command_completed
sta tmp0+1
lda cur_command_low,x
sta tmp0
jmp (tmp0)
command_completed
;; Step 6
lda #0
sta cont_subcom_high,x
+next_command
;; Step 7
lda command_list_high,x
beq nocommandlist
sta tmp0+1
lda command_list_low,x
sta tmp0
lda pcommand,x
tay
lda (tmp0),y ; Keep pointer prepared for the called routine
tay
lda command_high,y
sta smc_command+2
lda command_low,y
sta smc_command+1
smc_command
jmp $dead
nocommandlist
rts
.)