Pyrel dev log, part 2

Collapse
X
 
  • Time
  • Show
Clear All
new posts
  • Derakon
    Prophet
    • Dec 2009
    • 9022

    #46
    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:
    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])
    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.

    Comment

    • ekolis
      Knight
      • Apr 2007
      • 921

      #47
      Originally posted by Derakon
      ekolis: try using the number keys along the top of the main island. The numpad keys only work if numlock is off. There's a list of all currently-accepted commands in userCommand.py; the keys that map to those commands are in gui/wxPyrey/keymap.py.
      Nope, neither the regular number keys or the numpad do anything Do I need to focus any particular game window (console/map/log)? I might have forgotten a permutation there...
      You read the scroll labeled NOBIMUS UPSCOTI...
      You are surrounded by a stasis field!
      The tengu tries to teleport, but fails!

      Comment

      • ekolis
        Knight
        • Apr 2007
        • 921

        #48
        By the way, I didn't read your post on commands and inputs in full detail, but from what I skimmed, I like the concept! Sounds a bit similar to something I did in another game project once. As I recall, each type of command object in this game had a "get input" function, so once you told the game what you wanted to do, it could then prompt you for any further input it needed. So you select a spaceship (this was a 4X strategy game) and open the command menu and pick "move" - then the game instantiates a MoveCommand and calls its GetInput function, which in turn prompts the user to click a destination. Once the user does so, the destination is saved into the MoveCommand and the MoveCommand is added to the spaceship's command queue.

        I wonder if function currying (if it's supported by Python) could be useful in implementing this feature?
        You read the scroll labeled NOBIMUS UPSCOTI...
        You are surrounded by a stasis field!
        The tengu tries to teleport, but fails!

        Comment

        • Derakon
          Prophet
          • Dec 2009
          • 9022

          #49
          You do need to have mouse focus on the main window, not the message window (or the console you launched from). But it should do that automatically when you start the game.

          Originally posted by ekolis
          I wonder if function currying (if it's supported by Python) could be useful in implementing this feature?
          Function currying isn't a language feature, but you can fake it with lambdas:
          Code:
          def foo(a, b, c):
              print a, b, c
          bar = 10
          baz = lambda b, c: foo(bar, b, c)
          # baz is now a curried foo, kind of.
          As for using it to build up commands, I'm not really certain how that would work; you need the entire list of arguments for a verb before you can call its function. I suppose you could try to implement verbs as classes that examine their arguments and determine which ones still need to be resolved, but that sounds like it introduces extra complexity for no real gain in functionality...

          Comment

          • ekolis
            Knight
            • Apr 2007
            • 921

            #50
            Nope, still can't do anything, even with the map window focused - or even with the mouse hovering over it!

            Also, the map window seems to have the text drawn too big (or too small a space allocated for each symbol) - almost half of each symbol is cut off on the bottom!
            You read the scroll labeled NOBIMUS UPSCOTI...
            You are surrounded by a stasis field!
            The tengu tries to teleport, but fails!

            Comment

            • Derakon
              Prophet
              • Dec 2009
              • 9022

              #51
              Hm, that's odd. You must not be receiving keyboard events for some reason. Try opening up gui/wxPyrel/mainFrame.py and changing EVT_CHAR to EVT_KEY_DOWN. The problem with that is that you have to press the key once for every time you want to perform the action, which is annoying for movement.

              Another thing to try is moving the binding from the MainPanel class to the MainFrame class. Move these two lines:
              Code:
                      self.Bind(wx.EVT_CHAR, self.onKeyDown)
                      self.Bind(wx.EVT_PAINT, self.onPaint)
              and of course change "self.onKeyDown" and "self.onPaint" to "self.panel.onKeyDown" and "self.panel.onPaint".

              As for the drawing, clearly there's some platform-dependent variation on how characters are drawn...probably due to different fonts. I admit the current system was derived by tweaking values until they looked right to me; short of moving to a more sensible approach your best bet is to simply change the charWidth and charHeight values in gui/wxPyrel/artists/ascii.py.

              Comment

              • ekolis
                Knight
                • Apr 2007
                • 921

                #52
                Originally posted by Derakon
                Hm, that's odd. You must not be receiving keyboard events for some reason. Try opening up gui/wxPyrel/mainFrame.py and changing EVT_CHAR to EVT_KEY_DOWN. The problem with that is that you have to press the key once for every time you want to perform the action, which is annoying for movement.
                OK, now I can move, though it only works with the top number keys, not the numpad - regardless of if numlock is on or off!
                You read the scroll labeled NOBIMUS UPSCOTI...
                You are surrounded by a stasis field!
                The tengu tries to teleport, but fails!

                Comment

                • ekolis
                  Knight
                  • Apr 2007
                  • 921

                  #53
                  Originally posted by Derakon
                  As for the drawing, clearly there's some platform-dependent variation on how characters are drawn...probably due to different fonts. I admit the current system was derived by tweaking values until they looked right to me; short of moving to a more sensible approach your best bet is to simply change the charWidth and charHeight values in gui/wxPyrel/artists/ascii.py.
                  Looking at the wxPython documention, you should be able to set the *pixel* size of the font by calling:

                  self.font.SetPixelSize((self.charWidth, self.charHeight))

                  in gui/wxPyrel/artists/ascii.py in the __init__ method after you've initialized self.font. (For some reason the Font constructor won't let you initialize a pixel size, only a point size!) This improved the appearance of the game for me, though it looks like a little was chopped off on the bottom of a few symbols, so you might want to leave some extra space regardless. Oh, and do note the double parentheses - for some reason SetPixelSize takes only a tuple or a Size object, not two integers!
                  You read the scroll labeled NOBIMUS UPSCOTI...
                  You are surrounded by a stasis field!
                  The tengu tries to teleport, but fails!

                  Comment

                  • Derakon
                    Prophet
                    • Dec 2009
                    • 9022

                    #54
                    I guess the next thing to work on after sorting out commands and macros is a little cross-platform compatibility.

                    Seriously, thanks for all your work on this.

                    Comment

                    • ekolis
                      Knight
                      • Apr 2007
                      • 921

                      #55
                      Heh, no problem - all I did was test a few things and poke around in the code a tiny bit!
                      You read the scroll labeled NOBIMUS UPSCOTI...
                      You are surrounded by a stasis field!
                      The tengu tries to teleport, but fails!

                      Comment

                      • LostTemplar
                        Knight
                        • Aug 2009
                        • 670

                        #56
                        So, you basically copy an angband command processing method in your new variant.
                        However it is probably not the best method ever. You can split commands to avoid "prompts" completely, without even increasing number of key presses required, e.g. you can have "select target" "select item" and "fire" commands, this can, in general, make interface much more simple and fast (player does not have to use "select" commands too often).

                        Comment

                        • ekolis
                          Knight
                          • Apr 2007
                          • 921

                          #57
                          LostTemplar, you mean so instead of choosing fire, then choosing some ammo, then choosing a target, the game could remember your last-used item and target (Angband already has an option for the latter, but the former sounds useful too!) and just let you press the fire key to fire? And then there would be separate commands for "choose target" and "choose ammo"? (Interestingly, Steamband, with its firearms, does something similar - you can't fire ammo that isn't loaded into your firearm, logically enough, so you have to wield the ammo into the ammo slot, similar to the quiver!)
                          You read the scroll labeled NOBIMUS UPSCOTI...
                          You are surrounded by a stasis field!
                          The tengu tries to teleport, but fails!

                          Comment

                          • Derakon
                            Prophet
                            • Dec 2009
                            • 9022

                            #58
                            Originally posted by LostTemplar
                            So, you basically copy an angband command processing method in your new variant.
                            However it is probably not the best method ever. You can split commands to avoid "prompts" completely, without even increasing number of key presses required, e.g. you can have "select target" "select item" and "fire" commands, this can, in general, make interface much more simple and fast (player does not have to use "select" commands too often).
                            I'm not entirely clear on what you're talking about, but...

                            1) There's already a system in place whereby similar prompts use the same code, only differing by what message is displayed (e.g. "Examine which item" vs. "Fire which missile") and what callback function is used.

                            2) If you're talking about the "stuffing all the things needed to execute one command into a single Command object" bit, then the main issue there is ensuring that each Command has the appropriate context to execute itself. If you want to have separate "select item", "select target", and "fire" Commands, then the last one still needs to know which item to shoot and where to shoot it -- that context has to be made available to it somehow. Now, I will grant that every Creature has a "curTarget" field that marks the thing they're currently aiming at -- this is needed so you don't have to reselect a target every time you fire. But I don't want to go stuffing all that context into the Creature records, since said context is basically arbitrary (i.e. it depends on the verb used).

                            That said, of course to the extent that different Commands share the same code, they will re-use that code. I don't think this will be a problem.

                            Comment

                            • LostTemplar
                              Knight
                              • Aug 2009
                              • 670

                              #59
                              (Interestingly, Steamband, with its firearms, does something similar - you can't fire ammo that isn't loaded into your firearm, logically enough, so you have to wield the ammo into the ammo slot, similar to the quiver!)
                              Probably 99% of games do it in somewhat similar way.

                              Yes I know, that angband remember target also, but it is ugly, it requires an extra keypress to target "last target".

                              I suppose, to implement it in such a way, that when 'f' is peressed arrow is fired without questions, if there is not enough information, defaults should be used, e.g. first ammo at nearest enemy. Current angband behaviour is that there is a special command 'h' to do this, but if I want e.g. fire that particular ammo to nearest enemy in one keypress, I should create a macro. It would be much better imho to be able to press something like
                              'i' 'g' 'fffff' to bring up inventory, select ammo in slot 'g' and fire it 5 times to nearest (or last if it was selected) target instead of "fg5fg5fg5fg5fg5".

                              Comment

                              • ekolis
                                Knight
                                • Apr 2007
                                • 921

                                #60
                                I wonder if something similar could be done for spells? Now obviously you'll want to cast different spells sometimes, but a lot of the time you'll just want to spam magic missile or fireball or whatever your default "kill stuff" spell is! Typing "maa" over and over again is rather annoying - maybe if there was a hotkey for "cast default spell" and another command to mark a spell as your default? (Not just for attack spells - priests could cast Bless if they want, too!)
                                You read the scroll labeled NOBIMUS UPSCOTI...
                                You are surrounded by a stasis field!
                                The tengu tries to teleport, but fails!

                                Comment

                                Working...
                                😀
                                😂
                                🥰
                                😘
                                🤢
                                😎
                                😞
                                😡
                                👍
                                👎