Okay, since nobody suggested anything else as a priority, I'm going to continue working on handling input. In particular, I want to make design space for macros, and I want to improve how communication between Prompts and the game engine are handled. This got a bit long; apologies.
First off, in Pyrel I'm drawing a distinction between a Command and an Input. Inputs are things like pressing keys or clicking the mouse, and they are only ever exposed to the UI layer. Inputs are translated into Commands, which are conceptual descriptions of what change to the game state the player wants to make; these Commands are then handed to the game engine to execute.
So for example, the user hits '6'. The input layer translates this into 'Command(MOVE_6)' (the movement command constants still reference a numeric keypad for brevity). The engine receives this Command, and hands it to all Listeners; those Listeners then move to the right.
For simple, atomic actions like movement, this is straightforward. It gets more complicated when a given Input does not directly result in a Command, but requires more clarification. For example, the 'f' command results in the user being prompted for which ammunition they wish to fire, and then for a target to shoot the ammunition at. In terms of game flow, this means that 'f' is translated into a FIRE Command; the Listener then generates an ItemPrompt asking for ammunition; the callback for that Prompt generates a TargetPrompt asking for a target; the callback for that Prompt then actually fires the ammunition.
And we could imagine even more complicated interactions, for example a Chain Lightning spell that would bounce lightning bolts between user-selected points on the map:
* Cast a spell from which book?
* Cast which spell?
* Select tile 1
* ...
* Select tile N
* You do not have enough mana to safely cast this spell. Continue anyway?
Eventually we have to reach a "terminal" Prompt, where the user's answer finally causes that sequence of decisions to be inflicted upon the game world (i.e. the spell is cast). Remembering where we're ultimately headed as we bounce back and forth between Prompts and the Listener's Command-handling code makes for some rather unpleasant code -- every callback ends up having to have inherent context, likely supplied by lambda functions that take advantage of the local function scope. Uh...if you aren't familiar with lambdas and continuations then just take it from me that while they're very handy and make solving certain problems much more pleasant, over-relying on them can make your code rather ugly.
Anyway, we need a way to record all these decisions the player is making, so that we can play them back later for Macros. So it seems self-evident that we need an explicit way to contextualize the player's decisions, combine them together into a unified Command, and then hand the entire Command to the engine to execute. Thus instead of having the Command "Fire a missile", we have the Command "Fire a Mithril Arrow (+0, +0) at the closest enemy". Instead of the Command "Cast a spell" we have the Command "Cast the spell Chain Lightning from the book Incantations and Illusions, at these four tiles." These Commands can be recorded if desired, played back at a whim, and most importantly can be built up piece by piece as Prompts are generated and resolved.
Let's get back to that "fire a missile" Command. We start out with the verb "fire". This verb knows that it needs a direct object (a thing to fire) and an indirect object (something to shoot at). What it needs is a way to communicate those needs to the UI layer, interpret them, and pass the completed Command to the engine. We can imagine something like this:
Notice how we have an explicit "subject" for the Command that we are for now silently determining to be the player. In the future, we could use Commands to handle creature AI as well; this will be useful for letting them behave in complex ways like the player does.
Actually getting the genFireCommand() function to look like this requires some trickery, because Pyrel doesn't actually have modal interrupt prompts (where execution suspends until a prompt is resolved, and then picks up where it left off). However, we can fake it by a) isolating command-generator functions to their own threads, and b) how we write the gui.resolvePrompt function. The idea is that when an Input is generated, the UI layer (running in the main thread) spins up a new CommandGenerator thread, and then starts blocking. As long as a CommandGenerator thread exists, all further Input is directed to resolving any Prompts it generates. The CommandGenerator thread creates Prompts by inserting them into the UI layer via inter-thread communication (a simple system of locks and queues; there's potential for deadlock but you have to be actively trying to trigger it). The CommandGenerator then blocks on a response.
I imagine the above is wrong in various subtle ways, but once I start implementing it I should be able to sort any problems out pretty quickly. The basic structure seems sound.
Ultimately, we generate a Command that then needs to be interpreted by the game. Each verb has an associated function for interpreting Commands that use that verb -- thus, in the case of the Fire command, the verb's function knows that its inputs are, in order, the subject (firer), direct object (missile), and indirect object (target). Essentially what the CommandGenerator thread is doing is composing the arguments to this function.
Finally we get to macros -- macros are simply sequences of Commands. Notice that there is no Input here; everything is fully-abstracted from the UI layer. Recording input directly is a very easy way to handle macros, but it also is not very player-friendly -- we have all these workarounds involving inscribing @m1 on our spellbooks just to make certain that macros don't screw up. The only potential problems I can think of are:
1) Targeting. "Target nearest and fire" is a common macro, and the only reason it works is that Angband's targeting code automatically targets the nearest enemy. This can be readily handled by having a list of options brought up when targeting: "a) Target nearest hostile, b) Target myself, etc.". Of course you can ignore those and target manually, but you probably shouldn't do that when recording a macro since the macro will think you always want to target tile (12, 88) or whatever.
2) Firing ammo. Here we have the issue that ammo runs out, but we want the macro to simply switch to firing the next set of ammo in the quiver. I'm not entirely certain how to handle this, to be honest. Perhaps we can have a mode switch when selecting from inventory that selects the item in a specific slot, instead of the item with a specific name. This particular situation is the only one that has this issue, that I'm aware of, though.
So when you record a Macro, you simply walk through the series of Commands you want to execute, and when you stop recording, they are stored for later. Replaying a Macro simply involves feeding that sequence of Commands to the engine, one at a time.
First off, in Pyrel I'm drawing a distinction between a Command and an Input. Inputs are things like pressing keys or clicking the mouse, and they are only ever exposed to the UI layer. Inputs are translated into Commands, which are conceptual descriptions of what change to the game state the player wants to make; these Commands are then handed to the game engine to execute.
So for example, the user hits '6'. The input layer translates this into 'Command(MOVE_6)' (the movement command constants still reference a numeric keypad for brevity). The engine receives this Command, and hands it to all Listeners; those Listeners then move to the right.
For simple, atomic actions like movement, this is straightforward. It gets more complicated when a given Input does not directly result in a Command, but requires more clarification. For example, the 'f' command results in the user being prompted for which ammunition they wish to fire, and then for a target to shoot the ammunition at. In terms of game flow, this means that 'f' is translated into a FIRE Command; the Listener then generates an ItemPrompt asking for ammunition; the callback for that Prompt generates a TargetPrompt asking for a target; the callback for that Prompt then actually fires the ammunition.
And we could imagine even more complicated interactions, for example a Chain Lightning spell that would bounce lightning bolts between user-selected points on the map:
* Cast a spell from which book?
* Cast which spell?
* Select tile 1
* ...
* Select tile N
* You do not have enough mana to safely cast this spell. Continue anyway?
Eventually we have to reach a "terminal" Prompt, where the user's answer finally causes that sequence of decisions to be inflicted upon the game world (i.e. the spell is cast). Remembering where we're ultimately headed as we bounce back and forth between Prompts and the Listener's Command-handling code makes for some rather unpleasant code -- every callback ends up having to have inherent context, likely supplied by lambda functions that take advantage of the local function scope. Uh...if you aren't familiar with lambdas and continuations then just take it from me that while they're very handy and make solving certain problems much more pleasant, over-relying on them can make your code rather ugly.
Anyway, we need a way to record all these decisions the player is making, so that we can play them back later for Macros. So it seems self-evident that we need an explicit way to contextualize the player's decisions, combine them together into a unified Command, and then hand the entire Command to the engine to execute. Thus instead of having the Command "Fire a missile", we have the Command "Fire a Mithril Arrow (+0, +0) at the closest enemy". Instead of the Command "Cast a spell" we have the Command "Cast the spell Chain Lightning from the book Incantations and Illusions, at these four tiles." These Commands can be recorded if desired, played back at a whim, and most importantly can be built up piece by piece as Prompts are generated and resolved.
Let's get back to that "fire a missile" Command. We start out with the verb "fire". This verb knows that it needs a direct object (a thing to fire) and an indirect object (something to shoot at). What it needs is a way to communicate those needs to the UI layer, interpret them, and pass the completed Command to the engine. We can imagine something like this:
Code:
def genFireCommand(gameMap): # Get someone to do the firing -- for now, this is implicitly the player subject = gameMap.getContainer(container.PLAYERS)[0] # Get something to fire from the player directObject = gui.resolvePrompt(gui.prompt.ItemListPrompt("Fire what?", subject.inventory.filter(container.FIRABLES))) # Get something to shoot at indirectObject = gui.resolvePrompt(gui.prompt.TargetPrompt("Shoot at what?")) return userCommand.Command(userCommand.FIRE, [subject, directObject, indirectObject])
Actually getting the genFireCommand() function to look like this requires some trickery, because Pyrel doesn't actually have modal interrupt prompts (where execution suspends until a prompt is resolved, and then picks up where it left off). However, we can fake it by a) isolating command-generator functions to their own threads, and b) how we write the gui.resolvePrompt function. The idea is that when an Input is generated, the UI layer (running in the main thread) spins up a new CommandGenerator thread, and then starts blocking. As long as a CommandGenerator thread exists, all further Input is directed to resolving any Prompts it generates. The CommandGenerator thread creates Prompts by inserting them into the UI layer via inter-thread communication (a simple system of locks and queues; there's potential for deadlock but you have to be actively trying to trigger it). The CommandGenerator then blocks on a response.
I imagine the above is wrong in various subtle ways, but once I start implementing it I should be able to sort any problems out pretty quickly. The basic structure seems sound.
Ultimately, we generate a Command that then needs to be interpreted by the game. Each verb has an associated function for interpreting Commands that use that verb -- thus, in the case of the Fire command, the verb's function knows that its inputs are, in order, the subject (firer), direct object (missile), and indirect object (target). Essentially what the CommandGenerator thread is doing is composing the arguments to this function.
Finally we get to macros -- macros are simply sequences of Commands. Notice that there is no Input here; everything is fully-abstracted from the UI layer. Recording input directly is a very easy way to handle macros, but it also is not very player-friendly -- we have all these workarounds involving inscribing @m1 on our spellbooks just to make certain that macros don't screw up. The only potential problems I can think of are:
1) Targeting. "Target nearest and fire" is a common macro, and the only reason it works is that Angband's targeting code automatically targets the nearest enemy. This can be readily handled by having a list of options brought up when targeting: "a) Target nearest hostile, b) Target myself, etc.". Of course you can ignore those and target manually, but you probably shouldn't do that when recording a macro since the macro will think you always want to target tile (12, 88) or whatever.
2) Firing ammo. Here we have the issue that ammo runs out, but we want the macro to simply switch to firing the next set of ammo in the quiver. I'm not entirely certain how to handle this, to be honest. Perhaps we can have a mode switch when selecting from inventory that selects the item in a specific slot, instead of the item with a specific name. This particular situation is the only one that has this issue, that I'm aware of, though.
So when you record a Macro, you simply walk through the series of Commands you want to execute, and when you stop recording, they are stored for later. Replaying a Macro simply involves feeding that sequence of Commands to the engine, one at a time.
Comment