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.
Pyrel dev log, part 6
Collapse
X
-
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?
However, I like your idea better, and if you'll have to implement the necessary support for it anyway then all the better .
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.Comment
-
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
-
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.)
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
-
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...
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
-
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
-
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"]} }
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
-
Comment
-
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
-
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).
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
-
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
-
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"]}}
Code:{ "name": "nether bolt", "code": "launch projectile", "target": "creature", "damage": "level#5", "element": "nether", "message": ["{verb} a nether bolt", "cast"] }
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} }
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
-
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())
Code:target = yield gui.prompt.resolvePrompt(gui.promptTargetPrompt())
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
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
Comment