Pyrel dev log, part 6

Collapse
X
 
  • Time
  • Show
Clear All
new posts
  • Pete Mack
    Prophet
    • Apr 2007
    • 6883

    #46
    Originally posted by Derakon
    To be certain I understand your suggestion, the idea here would be that when the user hits Save, we start recording the changes they make to the game, and then when we get a spare chance (i.e. the user stops feeding in commands that change the game state), we make a copy of the game state and the "back out" the changes they made since they requested the save?

    That is an interesting idea. I do eventually want the game to be able to support rewind/replay capabilities, which are most efficiently represented as deltas to the game state. My thoughts for that was to enable spectating of the game -- when you wanted to watch another player, you'd request a copy of their game state, and then they would feed you deltas as game actions are taken. And of course, being able to undo actions makes for a great cheat option.
    Undo actions are a pain, but redo actions are trivially easy, and that's all you need to do in this case--I wrote one for angband in about a day, just using a save file and a keylogger. The only reason it took a day is because it was hard to figure out exactly where to inject keystrokes and mouse buttons into the I/O stack.

    Comment

    • Derakon
      Prophet
      • Dec 2009
      • 9022

      #47
      Yeah, redo is straightforward; just save the inputs. Undo will be necessary for AnonymousHero's concept and would be useful for spectating ("wait, what did he just do?" and similar).

      Comment

      • AnonymousHero
        Veteran
        • Jun 2007
        • 1393

        #48
        Originally posted by Derakon
        To be certain I understand your suggestion, the idea here would be that when the user hits Save, we start recording the changes they make to the game, and then when we get a spare chance (i.e. the user stops feeding in commands that change the game state), we make a copy of the game state and the "back out" the changes they made since they requested the save?
        I was thinking of something more simplistic where the save process would literally start from scratch if anything changed, sort of like optimistic concurrency control. (Based on the assumption that the player will eventually be idle for long enough for it to succeed.) That would mean that the saved state could actually be from a bit later than the user pressed "Save", which might be confusing, but I'm not sure it would matter in practice.

        However, I like your idea better, and if you'll have to implement the necessary support for it anyway then all the better .

        Originally posted by Derakon
        Regarding mutability: increasingly, more and more game-state-mutating actions are done by way of the GameMap (i.e. the primary object that holds all of the other objects). If I wanted to make the game immutable, then probably the way to do it would be to funnel all changes through the GameMap, directly or indirectly. Of course, then the trick becomes figuring out how to generate a new GameMap every time the player takes an action without it taking 4 seconds.
        That part is easy. Since you know exactly what needs to be modified (and we're assuming it's a small subset of the entire state), you can avoid copying things that don't change instead just referring to them. See Trees for an example of how that's done for trees. This generalized to any kind of data structure.

        Comment

        • Pete Mack
          Prophet
          • Apr 2007
          • 6883

          #49
          Undo logging is a BFD. To do it reproducibly you need to:

          a. Keep track of all monster positions (and possessions if you don't create objects on death.)
          b. Keep track of current map, including objects, monsters and positions,
          c. Keep track of player state (curHP, curMP, ....)
          d. Keep track of current RNG state (!?!?!?!) (I have no idea how to do this!!!!)
          e. Keep track of the current map.
          NOTE that all of these are logical actions, while the basic redo log is purely physical (given a save file.)

          This is a huge amount of work, and it doesn't make sense since the only real use for it is undo actions (aka cheating!)

          For replay (as per Derakon's example), all you need to do is apply the log up to the current position and freeze. Unless you go an insanely long time between save files, this is not a costly operation.
          In short: roguelikes are good candidates for replaying, but are really terrible candidates for true undo with backwards play.

          The physical analogy is the butterfly effect: time reversal is unstable given current incompletely stored state for the same reason that weather simulations are sensitive to tiny changes in initial conditions. More generally, roguelikes are not ergodic(!!!)

          PS: That means: don't try to model undos of non-reversible actions unless you are a serious BDSM sub and/or have way too much time on your hands.
          Last edited by Pete Mack; July 17, 2013, 10:38.

          Comment

          • Derakon
            Prophet
            • Dec 2009
            • 9022

            #50
            Originally posted by Pete Mack
            Undo logging is a BFD. To do it reproducibly you need to:

            a. Keep track of all monster positions (and possessions if you don't create objects on death.)
            b. Keep track of current map, including objects, monsters and positions,
            c. Keep track of player state (curHP, curMP, ....)
            d. Keep track of current RNG state (!?!?!?!) (I have no idea how to do this!!!!)
            e. Keep track of the current map.
            NOTE that all of these are logical actions, while the basic redo log is purely physical (given a save file.)
            It's a fair point that this may be harder than anticipated. Regarding RNG state, I'm going to have to figure this out so that RNGs can be stored in the savefile! Otherwise loaded saves will not behave deterministically, which is a big problem.

            All of your other items basically boil down to "keep track of the game state". This is all contained in the GameMap; as noted earlier, if I route all actual game state changes through the GameMap, then it becomes much easier to log them and (potentially) to undo them. But this would be a significant change and would greatly change how many objects function.

            That said, I'm speaking very blithely here and haven't actually devoted much thought to implementing undo in practice. It may well not be feasible to do it as a straight undo, in which case a better approach might be to make checkpoints (e.g. every 100 turns) and, when undo is triggered, load the checkpoint and then play forwards from there to the desired point. Of course, this requires saving/loading to be much faster...

            Comment

            • Pete Mack
              Prophet
              • Apr 2007
              • 6883

              #51
              Originally posted by Derakon
              It's a fair point that this may be harder than anticipated. Regarding RNG state, I'm going to have to figure this out so that RNGs can be stored in the savefile! Otherwise loaded saves will not behave deterministically, which is a big problem.

              All of your other items basically boil down to "keep track of the game state". This is all contained in the GameMap; as noted earlier, if I route all actual game state changes through the GameMap, then it becomes much easier to log them and (potentially) to undo them. But this would be a significant change and would greatly change how many objects function.

              That said, I'm speaking very blithely here and haven't actually devoted much thought to implementing undo in practice. It may well not be feasible to do it as a straight undo, in which case a better approach might be to make checkpoints (e.g. every 100 turns) and, when undo is triggered, load the checkpoint and then play forwards from there to the desired point. Of course, this requires saving/loading to be much faster...
              Actually, I was thinking you would do it differently: Make a save every (say) 2000 player turns (max) and for saves between this, use the redo log.

              Since the cost of storing the redo log is minimal, this lets you do saves in the background, from a clone of the map.


              And yes, you need to store the RNG. It is preserved in every roguelike since, well, rogue.

              Comment

              • Derakon
                Prophet
                • Dec 2009
                • 9022

                #52
                I just checked, and Python's random module has getstate() and setstate() functions. The state is, apparently, represented as a tuple of (3, length-625 tuple of ints, None) on my computer. That should be trivial to serialize and deserialize using my existing system.

                Numpy's random module is similar, though it of course uses a Numpy array for its long array of integers. I haven't yet decided if Pyrel is going to standardize on Numpy's random module (which can make large arrays of random numbers very quickly, which might be useful e.g. for dungeon generation) or Python's. It may be we can get away with using both, so long as they're both tracked properly.

                Your redo approach would also work. Some experimentation would be needed to determine the optimal frequency of checkpoints; it depends on the cost of making the checkpoint vs. the cost of replaying lots of actions.

                Comment

                • Derakon
                  Prophet
                  • Dec 2009
                  • 9022

                  #53
                  I'm letting save/load be for now. It works. It's not fast, but it works. And this is a roguelike; people shouldn't be saving/loading that often, so the fact that it's slow shouldn't interfere with play all that much.

                  I've implemented the first of the "screws with controls" status ailments. Confusion now prevents you from using any items whose "use verb" is "read" or "cast" (i.e. scrolls and spellbooks), and when you try to move, it will sometimes make you go in the wrong direction. The stats are:

                  * 30% of the time you go the way you want to go.
                  * Otherwise, 20% of the time, you go to one of the tiles next to the tile you wanted to go to.
                  * The remainder of the time, you go to a random adjacent tile or stay put (with even odds of all options).

                  There was an amusing bug with that last case: in the event that you stood still, the game would still try to "move" you. And the movement code checks for attackable entities in the target tile. So 1/9th of ~50% of the time, the player would punch themselves in the face when confused.

                  Implementation details: I added a "useVerb" field to items, which is a string like "read", "cast", "quaff", etc. Then the record for the Confusion status ailment was updated:
                  Code:
                          "name": "confusion", "shortName": "Cn",
                          "initialDescription": ["{verb} confused.", "are"],
                          "stackDescription": ["{verb} more confused", "are"],
                          "procs": {
                              "read": {"name": "prevent item use",
                                  "message": ["{verb} too confused!", "are"]},
                              "cast": {"name": "prevent item use",
                                  "message": ["{verb} too confused!", "are"]},
                              "move": {"name": "randomize movement",
                                  "successPercentage": 30, "adjacentPercentage": 20,
                                  "message": ["{verb} confused.", "are"]}
                          }
                  When you try to use an item, the game looks up the useVerb field for that item, and invokes procs that match the verb. The procs are handed the parameter set of the action; for item use, that parameter set currently just consists of whether or not the item should be allowed to be used, and whether or not the user should be charged energy for using the item (currently, trying to use an item when confused is "free"). But it could also include stuff like the failure rate, so that confusion would increase the failure rate on using staffs, etc.

                  The "prevent item use" Proc then sets the canUse boolean in that parameter set to False, and prints the message "You are too confused!". The "randomize movement" proc gets a similar parameter set from the MovementCommand, consisting of the direction the creature wants to move in and how far they want to move (always 1 in this case, but e.g. ToME2 DeathMolds phase door to move).

                  The overall system seems to be fairly straightforward to me. I haven't hooked up new command inputs mapped to the new use-item verbs, but that shouldn't be especially hard.

                  I've also updated some of the in-source-code documentation (the readme files) and fixed a few miscellaneous bugs. These changes are on my repo now.

                  Comment

                  • LostTemplar
                    Knight
                    • Aug 2009
                    • 670

                    #54
                    player would punch themselves in the face when confused.
                    It could be a cool feature, not a bug.

                    Comment

                    • Nick
                      Vanilla maintainer
                      • Apr 2007
                      • 9638

                      #55
                      Originally posted by LostTemplar
                      It could be a cool feature, not a bug.
                      "You hurt yourself in your confusion!"
                      One for the Dark Lord on his dark throne
                      In the Land of Mordor where the Shadows lie.

                      Comment

                      • Derakon
                        Prophet
                        • Dec 2009
                        • 9022

                        #56
                        I actually briefly considered leaving that in, but letting the player perform full-strength melee attacks against themselves seems a trifle cruel.

                        Next step: I'd like to tackle spellcasting. Structurally this is a bit tricky:

                        1) We have spellbooks. Spellbooks are just container objects that come pre-loaded with certain spells.
                        2) We therefore have spells. Spells are objects that, when used, cast a spell.
                        3) We need to attach not just the spell effect to the object, but also the failure rate and mana cost. These are different for different classes.

                        Functionally, each spell has a cost, a difficulty, and a level at which it can be learned, and these are different for each class. As far as I'm aware, Vanilla doesn't use any kind of automatic scaling (e.g. rogues simply learning spells at 2x the character level at which mages learn them); the values are hard-coded for each instance. However I admit I haven't managed to track down where exactly in the source code this stuff is handled (it's sure not in edit/spell.txt).

                        I'd like for Pyrel to have sane defaults, using multipliers, so that values don't have to be filled in for every single spell. That is, each spell would have a baseline difficulty, minimum character level, and cost, and depending on what class you are a multiplier and addend would be applied to each. Of course these would be overridable.

                        Getting back to spell effects, each spell would need to have the following procs:

                        1) on "cast" (i.e. the useVerb field for a spell), update the failure rate of trying to perform the spell, based on the stats of the caster and the difficulty of the spell. This value would also be modified by status ailment procs. The player's mana pool is decreased by the calculated cost of the spell.
                        2) on "item use" (i.e. assuming the failure rate is passed), the spell takes effect. Acquire any necessary targeting information at this time.

                        For the time being, I think failure rate handling will be done outside of procs, as part of the "use an item" command logic. I can envision a way it could be moved to a proc, but I don't really want to tackle that right now; if/when it becomes a priority it should be able to be moved easily enough.

                        Comment

                        • kaypy
                          Swordsman
                          • May 2009
                          • 294

                          #57
                          Originally posted by Derakon
                          I'm letting save/load be for now. It works. It's not fast, but it works. And this is a roguelike; people shouldn't be saving/loading that often, so the fact that it's slow shouldn't interfere with play all that much.
                          A relatively fast save may still be useful- frequent backup saves are especially useful in early to mid development.

                          Originally posted by Derakon
                          Functionally, each spell has a cost, a difficulty, and a level at which it can be learned, and these are different for each class. As far as I'm aware, Vanilla doesn't use any kind of automatic scaling (e.g. rogues simply learning spells at 2x the character level at which mages learn them); the values are hard-coded for each instance. However I admit I haven't managed to track down where exactly in the source code this stuff is handled (it's sure not in edit/spell.txt).
                          If you are still interested in checking it out before rewriting everything, its all in the B entries of p_class.txt. Thus each spell has a different level, cost, failrate, and firstcast-xp for each class. And it is almost, but not quite, completely indecipherable 8-)

                          Hmm. If you are going with a default based approach, you may want to have some way to give spells arbitrary tags. So you could for example tag all the damage dealing spells as "combat" and then set the rogue as "all spells labelled 'combat' are unavailable"

                          Comment

                          • Derakon
                            Prophet
                            • Dec 2009
                            • 9022

                            #58
                            Originally posted by kaypy
                            A relatively fast save may still be useful- frequent backup saves are especially useful in early to mid development.
                            Sure, but after beating my head against it for awhile I've made no real progress. If someone else wants to work on optimizing it they are more than welcome to do so; the only remaining idea I have is to rewrite the serialization logic in C/C++ (NB Cython doesn't make an appreciable difference).


                            If you are still interested in checking it out before rewriting everything, its all in the B entries of p_class.txt. Thus each spell has a different level, cost, failrate, and firstcast-xp for each class. And it is almost, but not quite, completely indecipherable 8-)
                            Heh, thanks for the pointer. One big advantage Pyrel's datafile format has over the edit files is that all of the fields are labeled (since they're JSON dicts).

                            Hmm. If you are going with a default based approach, you may want to have some way to give spells arbitrary tags. So you could for example tag all the damage dealing spells as "combat" and then set the rogue as "all spells labelled 'combat' are unavailable"
                            This is a good idea! We could also make different classes have differing aptitudes for different spell types. E.g. mages would be pretty good at everything, rangers would be good at healing (for the one arcane healing spell) and have okay aptitude at mobility and attack spells, rogues are good at detection, etc. And it's easy to do by just using the object template system.

                            Comment

                            • Derakon
                              Prophet
                              • Dec 2009
                              • 9022

                              #59
                              I've run into a bit of a design conundrum with spells. Specifically, there's a framework for monster spells, and I'm trying to figure out to what extent I want to be able to re-use it for player spells, and what that would entail.

                              Monster spells are typically just referred to by name. For example, here's the record for the Filthy Street Urchin at the moment:
                              Code:
                              {"index": 1, "name": "Filthy street urchin",
                                "display": {"ascii": {"color": "L_DARK"}},
                                "templates": ["townsfolk"],
                                "stats": {"alertness": 40, "evasion": 5, "maxHitpoints": 3, "speed": 1, "visionRange": 4},
                                "nativeDepth": 0, "rarity": 2, "experienceValue": 0,
                                "blows": [{"verb": "beg"}, {"verb": "touch", "proc": "eat_gold"}],
                                "flags": ["MALE", "FRIENDS", "RAND_25", "OPEN_DOOR", "picks up items"],
                                "description": "He looks squalid and thoroughly revolting.",
                                  "magic": {"frequency": 1, "spells": ["nether bolt", "confuse"]}}
                              And here's the corresponding spell entry for nether bolts:
                              Code:
                                  {
                                      "name": "nether bolt",
                                      "code": "launch projectile",
                                      "target": "creature",
                                      "damage": "level#5", "element": "nether",
                                      "message": ["{verb} a nether bolt", "cast"]
                                  }
                              When the urchin's AI decides to cast a spell, it picks a name from its spell list and goes to spell.py to map that into a Spell instance. spell.py is responsible for loading spell data; it also handles situations in which a monster has a custom spell (e.g. if the urchin's nether bolt was supposed to be unusually powerful or have strange side-effects).

                              Note that spell.py is not part of the main "game engine"; it's in the proc code. As a result, the engine literally does not know about any of the spell definitions, even though they're basically just customized Procs (for example, nether bolt uses the "launch projectile" Proc). The creature AI handles selecting a Spell to cast, selecting a target, displaying any appropriate messages, and updating the player's monster memory (assuming they saw the caster).

                              Now, it'd be nice if we could re-use spell definitions for player spells. It's a nice bit of symmetry and there's no real reason to duplicate stuff. I see two main ways to go here:

                              1) Add a "cast spell" Proc that does similar logic as is currently in the AI logic. Then all of the player's spells would have entries that look something like this:
                              Code:
                              {"templates": "arcane attack spell",
                              "nameInfo": {"variantName": "Magic Missile"},
                              "procs": [{"name": "cast spell", "spellName": "magic missile", 
                                  "damage": "(3+L/8)#4", "triggerCondition": "item use"}],
                              "spellInfo": {"manaCost": 1, "baseFailureRate": 25, "minLevel": 1}
                              }
                              2) Merge the spell Procs into the main Procs list (so we'd ditch spell.txt and move all of the spell definitions into proc_template.txt) and do away with the concept of a Spell entirely.

                              Approach 2 sounds good, but leaves us with the problem of how to handle updating monster memory; I'd planned to eventually have Spells handle their own display logic (including e.g. damage ranges or descriptive tooltips). That becomes considerably more difficult if Spells are literally just Procs. Meanwhile, approach 1 will tend to leave us with a ton of Procs that are just redirects to another Proc, solely for the reason that the effect of the Proc is the same as the effect of a creature spell.

                              Neither of these sound really ideal to me...

                              Comment

                              • Derakon
                                Prophet
                                • Dec 2009
                                • 9022

                                #60
                                I went ahead with the "cast a spell" Proc approach, salving my conscience a bit by calling it "cast creature spell" to make it a bit more clear what exactly is going on here. Basic spellcasting works now:



                                However, if the spell needs more information from the user (for example, a target to cast the spell on), then we're sunk. When I originally wrote Pyrel's Prompt system, I split things into separate threads so we could have a main execution context and a context in which input was provided by the user, with the result that we could, from anywhere in the code, halt execution to get more information from the user. At some point this was rewritten by someone else to use coroutines instead. Unfortunately, Python's coroutines aren't very elegant, and I see no way to re-enable this "stop anywhere to get more input" functionality without plumbing "yield" functions throughout the codebase. I'd love to be proven wrong.

                                What I'm looking for is for a random bit of code anywhere in the system to be able to do
                                Code:
                                    target = gui.prompt.resolvePrompt(gui.prompt.TargetPrompt())
                                , with resolvePrompt() suspending the current execution context until the Prompt has been executed. Right now, you can only accomplish this from within a Command, where you instead do
                                Code:
                                    target = yield gui.prompt.resolvePrompt(gui.promptTargetPrompt())
                                Note the yield statement, which is a necessary aspect of using coroutines (as far as I can tell, anyway).

                                Of course, technically the "cast a spell" logic is being handled from within a Command -- but in order to be able to call resolvePrompt() in the above manner, we would have to have every single function from the Command's contextualizeAndExecute() through to the Proc's trigger() be invoked like this:
                                Code:
                                for i in func():
                                    yield i
                                I flat-out refuse to use such garbage invocation syntax throughout Pyrel's codebase. It's fine in isolated circumstances, but we're basically talking about every single bit of "actually do something" code working in this fashion.

                                If I can't figure out how to use the current coroutine system to do what I want, I'll just rewrite it to use threads. The thread-based system worked, as I recall; it just required some care to avoid common threaded coding pitfalls. But once it's been written, it shouldn't need to be touched at all often.

                                Comment

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