Roguelike object model theorycrafting

Collapse
X
 
  • Time
  • Show
Clear All
new posts
  • AnonymousHero
    Veteran
    • Jun 2007
    • 1393

    #16
    Originally posted by Derakon
    I have to admit I haven't done much work with immutable objects being central to a large-scale project. I'm not certain what you mean by "pushing state mutation to the edge of the program"; care to explicate? I do recognize the value in being able to call a function with a specific argument and knowing that that function won't change that argument though.
    Sure -- I'll try to respond in-depth on this tomorrow.

    Originally posted by Derakon
    A style guide protects you from people screwing up if you forbid checkins that don't follow the style guide. Of course you could still theoretically write and check in code that breaks the guide. It's like how most door locks are just there to keep honest people from being tempted, not as any significant form of security. Just because they can be relatively easily bypassed doesn't mean they will be.
    I was thinking of the specific situation of code like
    Code:
    x.aFieldName = 123
    where you really meant
    Code:
    x.aFieldname = 123
    Reviews may help in detecting such cases, but this kind of mistake is a) difficult for humans to detect (we're annoyingly good at "overlooking" such mistakes because of our overactive pattern matching esp. when reading) and b) hard to detect by testing.

    I'd much rather have
    Code:
    x.aFieldName = 123
    throw an exception immediately rather than simply creating a new attribute on "x" named "aFieldName" (and leading to subtle bugs later). This would make such typos much easier to detect in testing since the error would trigger as soon as you hit that line.

    You might say I'm a sucker for the mantra of "detect errors/failure as early as possible"... which is not easy with dynamically type checked languages like Python, but every little helps.

    Comment

    • ekolis
      Knight
      • Apr 2007
      • 921

      #17
      Originally posted by AnonymousHero
      However, I don't see any particular reason that the monster instances, object instances, etc. shouldn't be immutable.
      Wouldn't that require instantiating a new monster instance whenever an existing monster instance takes damage? Or are you saying that only the references themselves should be immutable, but their attributes can be mutable?

      e.g.

      Monster m = SomeFunctionWhichGetsAMonster();
      m.Hitpoints -= 5; // ok
      m = SomeOtherFunctionWhichGetsAMonster(); // not ok
      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

        #18
        Yes, I agree that it'd be preferable to error instead of creating a new field. And you're right that this isn't the kind of thing that code reviews will necessarily catch. Unfortunately, while it is apparently possible to enforce no new field creation post-constructor, it requires some rather unpleasant hackery since there appears to be no way to swap out the __setattr__ function at runtime.

        Comment

        • AnonymousHero
          Veteran
          • Jun 2007
          • 1393

          #19
          Originally posted by ekolis
          Wouldn't that require instantiating a new monster instance whenever an existing monster instance takes damage? Or are you saying that only the references themselves should be immutable, but their attributes can be mutable?
          Yes, a new instance. In the case of Python's named tuples, what you'd do is
          Code:
          >>> p = Point(x=11, y=22,z=44)
          >>> p2 = p._replace(x=33, z=55)
          Point(x=33, y=22,z=55)
          The idea is that: 1) you just copy all the fields except the ones you override, and 2) if you make sure that your named tuples only refer to other immutable objects, then you can use shallow copies rather than deep copies of the fields. The latter makes the "update" fast.

          Comment

          • AnonymousHero
            Veteran
            • Jun 2007
            • 1393

            #20
            Originally posted by Derakon
            Yes, I agree that it'd be preferable to error instead of creating a new field. And you're right that this isn't the kind of thing that code reviews will necessarily catch. Unfortunately, while it is apparently possible to enforce no new field creation post-constructor, it requires some rather unpleasant hackery since there appears to be no way to swap out the __setattr__ function at runtime.
            That doesn't look too bad to me. Obviously you'd want to hide this away in a decorator or something for easy use, so you'd be able to write something like:

            Code:
            @struct
            class X:
                def __init__(self, x, y, z):
                    self.x = x
                    self.y = y
                    self.z = z
            and have the decorator take care of the rest.

            Comment

            • AnonymousHero
              Veteran
              • Jun 2007
              • 1393

              #21
              Originally posted by Derakon
              I have to admit I haven't done much work with immutable objects being central to a large-scale project. I'm not certain what you mean by "pushing state mutation to the edge of the program"; care to explicate?
              Here's a more in-depth response:

              Let's start by imagining the whole game state as mutable. That is, monster (instances) are mutable, player object is mutable, items are mutable. In addition the list of monsters on a level is mutable, the list of items on a level is mutable, etc. (So Thing and Set are both mutable.). Further, the entire game state (which points to the list of monsters, the list of items, etc. etc.) is itself mutable.

              The idea then is to start by making the low-level bits immutable. This means that you can hand any function a "monster" value/reference or an "item" value/reference and not have to worry whether it's actually mutated or not, because it can never be. You'll know if the function can "change" the monster because it'll have to return a new monster instance instead of changing it in-place. This is the lowest level.

              Now we move the same idea up a level into the collections of things (Set) -- you never actually modify the list of monsters directly (you can't, it's immutable), you just return a modified "copy" of the list with your modifications applied. (Don't worry this can be done efficiently, see "persistent collections".)

              ... and so on until we have the entire game state as being immutable. That is, if a function receives a reference to the whole game state then it can only "modify" bits that it returns. This is usually where it's a good idea to stop, so the reference to the game state is mutable, but all the referred to data is not.

              In this view, the "main loop" essentially becomes
              Code:
                  state_ref = tick(state_ref)
              (That is, you simply replace the old state reference with a reference to the new state. This is also efficient because you don't have to clone -- you can simply reuse references to whatever old state wasn't modified.)

              I hope that makes the idea a little clearer.

              Comment

              • Derakon
                Prophet
                • Dec 2009
                • 9022

                #22
                I see, thanks. It would seem like you'd have to use some nonstandard types to achieve this (the aforementioned "persistent collections"), but I can see the value at least at the lower level of immutable players and monsters and so on. Perhaps I'm missing something though, but it seems like if you wanted the entire game state to be immutable in this way then you'd have to do a lot of "destroying" the entire game state to replace it with a new one. Of course you aren't actually doing anything like that behind the scenes but I imagine that it'd make the code a bit messy. I mean, how do you avoid this?
                Code:
                gameState = gameState.updatePlayer(player.wield(newItem))
                gameState = gameState.updateMonster(monster, player.attack(monster))
                In other words, as the game state contains the entirety of the game, it seems like you'd have to create a "new" one every time any facet of it changes.

                Comment

                • AnonymousHero
                  Veteran
                  • Jun 2007
                  • 1393

                  #23
                  Originally posted by Derakon
                  I mean, how do you avoid this?
                  Code:
                  gameState = gameState.updatePlayer(player.wield(newItem))
                  gameState = gameState.updateMonster(monster, player.attack(monster))
                  You don't avoid it -- you embrace it!

                  I realize it may look quite alien at first, but I find that in the long run being explicit about state updates (rather than "allowing" them anywhere) is a huge help rather than a hindrance.

                  Functional languages do tend to have slightly better support for this style of programming so that you can avoid a little of the verbosity. In a language like, say, Python or Java you might settle on a compromise where e.g. the "dungeon level" object is a structure containing mutable references to immutable "monsters", "items", etc collections. That'd probably be about the right level of immutability for those languages.

                  Anyway, this is getting pretty far off-topic, so I'll leave it there .

                  Comment

                  • Derakon
                    Prophet
                    • Dec 2009
                    • 9022

                    #24
                    Well, I guess this was inevitable. I've started implementing this. 954 lines of Python is enough to get me the following:

                    * Two freely-resizable windows, one showing a map and the other a scrollable message list (actually it's capturing stdout, so I can write to it with print statements).
                    * A very simple map generator that creates an 80x24 arena with two creatures (a wanderer and the player) and an item.
                    * The wanderer will move about at random, but won't move through walls or the player.
                    * The player can pick up the item, look at his inventory, equip the item, look at his equipment, and attack the monster.
                    * Attack damage is calculated differently based on the weapon(s) the player has equipped.



                    Obviously I haven't spent much time on the display code yet.

                    Some code snippets:

                    Code:
                    ## IDs for "fundamental" containers that will recur elsewhere in the code.
                    (
                    ## Must implement receiveAttack(self, alt)
                    ATTACKERS,
                    ## Must implement update(self, gameMap)
                    UPDATERS,
                    ## Must implement onCommand(self, gameMap, command)
                    LISTENERS,
                    ## We just like to keep track of this guy.
                    PLAYERS,
                    ## Must implement canMoveThrough(self, alt)
                    MOVERS,
                    ## Must implement pickupItem(self, item), removeItem(self, item)
                    CARRIERS,
                    ## Must implement pickup(self), drop(self), examine(self).
                    ITEMS,
                    ## No required functions
                    WIELDABLES,
                    ## Must implement getMeleeDamage(self)
                    ITEMS_MELEE) = range(9)
                    Code:
                        ## Receive a command from the user and propagate it to our listeners.
                        # Return a list of prompts that need to be resolved based on the command.
                        # \param source The origination point of the command; needed for 
                        # interactive commands (e.g. requiring a list to be displayed).
                        def onCommand(self, source, command):
                            result = []
                            if container.LISTENERS in self.idToContainerMap:
                                for thing in self.idToContainerMap[container.LISTENERS]:
                                    prompt = thing.onCommand(self, source, command)
                                    if prompt:
                                        result.append(prompt)
                            return result
                    Code:
                        ## Try to move the given Thing from the first position to the second. 
                        # If there are obstructions, return a Container holding them. Otherwise,
                        # update where we store the Thing (and its 'pos' field).
                        def moveMe(self, thing, source, target):
                            x, y = target
                            blockers = self.cells[x][y].filter(container.MOVERS)
                            result = container.Container()
                            for blocker in blockers:
                                if not blocker.canMoveThrough(thing):
                                    result.subscribe(blocker)
                            if not result:
                                # It can move there, so move it.
                                self.cells[x][y].subscribe(thing)
                                self.cells[source[0]][source[1]].unsubscribe(thing)
                                thing.pos = target
                            return result
                    There's still plenty of not-so-great code in here; for example, drawing is currently atrociously inefficient. Plus there's a bunch of placeholder do-nothings or bad default behaviors that need to be filled in. And of course, there's no real content to speak of. Still, the concept is illustrated.

                    If you want to download the code, it's in a tarball here. It needs Python and wxPython installed to run (do "python pyrel.py" to run it) but should have no other dependencies.

                    Comment

                    • RogerN
                      Swordsman
                      • Jul 2008
                      • 308

                      #25
                      Originally posted by ekolis
                      Didn't someone a year or two ago produce a reasonable facsimile of Angband written in C#? I think it was called "Cryptband"...
                      Cryptband was a fun project while it lasted (fun to write at least). It's Angband-ish but frankly ZAngband probably has more in common with vanilla than Cryptband does.

                      Unfortunately I consider Cryptband to be a failure in terms of design. Refactoring was going to be a lot of work and I ended up using messy hacks to save time. Frankly I succumbed to the temptation of borrowing too much code from Angband, when a proper OO port should have done things radically different.

                      Long story short... I don't recommend using that project as a starting point for anything else

                      Comment

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