(Newbie Warning) Coding Rogue-Clone in Python

A place to discuss the implementation and style of computer programs.

Moderators: phlip, Moderators General, Prelates

(Newbie Warning) Coding Rogue-Clone in Python

Postby The Great Hippo » Sun May 13, 2012 4:07 pm UTC

Hello! I'm hoping to get a little coding advice, and would love to hear some input from more experienced programmers (basically, that means everyone) on how to best pursue this project of mine.

I'm trying to make a rogue-clone in Python using the excellent libtcod library. It's a project I've picked up and put back down over the years, so I've got a lot of code for it just floating around. The goal is a rogue-clone with pre-built dungeons, dialogue trees, and highly modifiable content (I know, I know, that's really not a rogue-clone anymore, but bare with me). The end result I'm aiming for is a cooperative story-telling tool that lets you build a character and carry them through multiple stories/quests/modules that others have written (and acquire module-specific items, classes, and other things from these individual stories that stay with you).

The architecture I imagined using to accomplish this way back in the day is starting to look shakey, though. Here's the basics: Everything in the game (Players, monsters, levels, items, spells) that's more complex than a single value is a member of the Object class. The Object class has two important bits: 'Properties' (a dictionary that starts with no elements) and 'Container' (a list that starts with no elements).

Anything in the 'Container' list modifies its parent (so if I have an object that's, say, a level of Fighter, and I drop it in the Player's 'container', they gain a level in Fighter). Anything in the properties list tells the game how to treat the Object (one property has the key 'Blocked'; it's either True or False. If True, this object blocks movement). Since Python's dictionary lets me set anything as a value to a presiding key, I can even handle inventory this way (Inventory is a key in the Properties dictionary, its value is a list that contains the objects in your inventory).

One of the first big problems I encountered with this structure is that it makes defining 'new' instances of objects very tricky (for the longest time, I couldn't get the code to tell the difference between the NPC and the PC; when I modified one in-game, the other was modified simultaneously). I got through this problem with the DeepCopy function, though (which forces the code to create a new instance of an object). I'm worried now that as I get deeper into figuring out how the properties and container interact, that problem is going to magnify--and I'm going to end up at serious risk of doing things like having a Object A have Object B in its container, and Object B also have Object A inside its container (one of the solutions I came up with was to use reference numbers and a search function rather than actually putting the object inside the container, but this seems inelegant and inefficient--and still open to exactly the same problem, just with one degree of separation).

I'm not a very experienced programmer, so I apologize if any of the above comes off as clumsy or awkward--if someone has suggestions on a better architecture (or thoughts on how I could improve this architecture, or how this architecture could work, or what challenges this architecture might present in the future), I would absolutely love to hear them.

Thanks for reading!
User avatar
The Great Hippo
 
Posts: 5489
Joined: Fri Dec 14, 2007 4:43 am UTC
Location: behind you

Re: (Newbie Warning) Coding Rogue-Clone in Python

Postby Ben-oni » Sun May 13, 2012 9:10 pm UTC

There are two approaches to the problem. It depends on how you see things.

1) Cyclic dependencies are ok
2) Cyclic dependencies are bad

Say, for instance, that Character A is holding sword X which has had a spell of true ownership cast upon it so that A is X's owner, something that cannot change when A drops X. A and X are, therefore, cyclically dependent. Searching to see if A depends on B would require graph traversal instead of tree traversal, but that's not a problem, is it?

On the other hand, you might say a property isn't a real dependency, and you want only to make sure that containers can't contain themselves. In that case, make containers immutable and cyclic dependencies can never arise. Ha, if only it were that easy. Mutable graphs are hard.

Basically, what you need to do is think about the structure of the data and how your program alters that structure over time. What operations are legal, and which are not? Design so that invalid operations cannot happen. When doing this, remember that language provided structures are only a tool for implementing your structure. You should design without them first, so you don't get locked into thinking about your program only in the terms of your language.
Ben-oni
 
Posts: 268
Joined: Mon Sep 26, 2011 4:56 am UTC

Re: (Newbie Warning) Coding Rogue-Clone in Python

Postby The Great Hippo » Sun May 13, 2012 9:28 pm UTC

Right, okay--thanks! What are some of the consequences of cyclical dependencies, though? Are they something that can crash the program? Unless thoroughly accounted for, will they increase the program's instability? Will they slow the program down significantly (since this is my first stab at a real, finished, thoroughly complex program, I'm not terribly concerned with efficiency, but I will be if I create a situation where the program slows to a crawl on any relatively normal system)? I'm concerned about them, but I'm not sure if I should be concerned--they sound like something that's bad, but I don't actually know how or why they're bad.

I'll look up some of the terms you're using later. I think I understand what you're saying re: mutable and immutable graphs versus trees, but I want to make sure. As I understand it, Python is very tricky re: how it handles these things.
User avatar
The Great Hippo
 
Posts: 5489
Joined: Fri Dec 14, 2007 4:43 am UTC
Location: behind you

Re: (Newbie Warning) Coding Rogue-Clone in Python

Postby Ben-oni » Sun May 13, 2012 10:38 pm UTC

Cyclic dependencies are something that are neither good nor bad. Personally, I try to stay away from them, as I consider them inherently error prone. A common scenario is where object A contains object B, and then object B refers to object A as it's "container": sometimes this is useful, and at other times it can lead to errors if the property isn't properly maintained as an invariant.

However, when the natural structure of the data is a graph*, cyclic dependencies are simply part of the nature of what you're dealing with. It looks to me, however, that what you want to do is to ensure a strict tree hierarchy on the containers, while allowing properties to be flexible and arbitrary.

The easiest way to do this is probably to use runtime checks when you add an object into a container: make sure the object being added isn't already in a container, say by having an "owner" field that you can check against, and then making sure the object you're adding it to isn't in it's container hierarchy. If it is, raise an exception. This could mean a lot of tree traversal each time you add something to a container, but if the complexity and size of the data structures is relatively low, and the insertion operation is relatively uncommon, this shouldn't be a problem.


*All** data is graphic. I'm referring to cases where it can't be narrowed to a more precise form, say, for instance, a lattice or a tree.

**Except when we're dealing with an abstract meta-representation of data. But that's math, not coding.
Ben-oni
 
Posts: 268
Joined: Mon Sep 26, 2011 4:56 am UTC

Re: (Newbie Warning) Coding Rogue-Clone in Python

Postby The Great Hippo » Sun May 13, 2012 11:39 pm UTC

Just as an aside, this is really helping me to think about this more critically. I've been bouncing these ideas in my head forever and I've developed a number of blindspots.
Ben-oni wrote:Cyclic dependencies are something that are neither good nor bad. Personally, I try to stay away from them, as I consider them inherently error prone. A common scenario is where object A contains object B, and then object B refers to object A as it's "container": sometimes this is useful, and at other times it can lead to errors if the property isn't properly maintained as an invariant.
I can't actually think of a situation where Object B would refer to Object A as its container, but part of the thing here is that I've got two 'types' of objects going on, and I haven't clarified which is which: Some of these objects are instanced, some of them are not.

For example, a rusty dagger is going to be instanced. Every rusty dagger is a new object, cut from the mold of the 'rusty dagger' object (the first, platonic rusty dagger). This is because the properties of each rusty dagger can be different (here's a rusty dagger on the floor. It has an x-y-z coordinate, which are all values contained in its properties. It also has a property of whether or not it's broken. The platonic rusty dagger has no x-y-z coordinate, and its broken property is always set to False--because I need to keep it 'safe' from modification, otherwise all instances of rusty daggers I copy from it would carry those 'changed' properties over--all broken daggers henceforth generated would be broken). A level in Fighter, however, never needs to be instanced, because this isn't something that's ever going to change for any foreseeable reason; every time I put a level of Fighter in the container of Object A, it can be the same singular instance of that object everywhere, and that's actually much easier on me because if I suddenly decide to change the properties of a level in Fighter I only have to do it once (and since all references to this level refer to the same instance...).

There's a function that will search Object A's container, pull the properties of every object in this container, then somehow apply them to Object A. I'm a little worried about this, because I'm not sure if there's going to be a need to look at the containers of all the Objects in Object A's container, and pull their properties... I'm thinking I definitely won't want to do this, because it creates all sorts of problems and confusions, but I'm worried that a situation might arise where I have to do it (although, now that I'm thinking about it, complex objects--objects not designed to pass their attributes to their 'owner' through the 'container' list--will never have reason to be put in a container).

Actually, I think I just solved my own problem. Does this sound reasonable? Objects that modify their parents (i.e., the object that contains them) always have empty containers. There's no reason for the level of Fighter to contain anything; there's no reason for the rusty dagger to ever be contained. So I seem to have two actual types of objects--one with containers, one without. I just then have to create a structure that never allows an object with a container to be put inside of a container.
User avatar
The Great Hippo
 
Posts: 5489
Joined: Fri Dec 14, 2007 4:43 am UTC
Location: behind you

Re: (Newbie Warning) Coding Rogue-Clone in Python

Postby Ben-oni » Mon May 14, 2012 4:18 am UTC

The Great Hippo wrote:Actually, I think I just solved my own problem. Does this sound reasonable? Objects that modify their parents (i.e., the object that contains them) always have empty containers. There's no reason for the level of Fighter to contain anything; there's no reason for the rusty dagger to ever be contained. So I seem to have two actual types of objects--one with containers, one without. I just then have to create a structure that never allows an object with a container to be put inside of a container.

This is a clear use case for a type system, which would raise a compile time error if the wrong type of object tried to pass into a container.

In Python, you can use runtime checks by creating a single function to add objects to containers: if it find the object in question already has a container, it throws an error.

But why shouldn't objects be allowed to nest? Image: I pack my lunch in a bag, and then pick up the bag. Of course, your hierarchy isn't about physical containment, but it should be dealt with at some level. Perhaps you have a property "contents", which is a list of objects. In this case, I would want to vigorously enforce that no object can be in the contents of more than one object, and that no circular nesting can be performed.

There is something else you probably want to think about. How do contained objects interact? That is, since the container gains the properties of the objects in it's container, what if there is a conflict? One sets attribute P to X, another sets P to Y. How is the conflict resolved? Or, suppose character C has a level of Fighter and then picks up a level of Wizard. How would you write a rule that says a multiclass character takes experience penalties? After all, the level of Fighter knows nothing at all about the level of Wizard, so the properties they set/modify can't take that into account.
Ben-oni
 
Posts: 268
Joined: Mon Sep 26, 2011 4:56 am UTC

Re: (Newbie Warning) Coding Rogue-Clone in Python

Postby The Great Hippo » Tue May 15, 2012 2:35 pm UTC

I'm investigating the use of a type system now--I have a few ideas how to make it. And as for the inventory thing--yeah, that was my thought--just make 'inventory' a property that contains a list, with the list containing instanced occurrences of objects, and either checks to make sure the same instance of an item doesn't end up in a different inventory (or even the same one!)--or otherwise prevents that from ever occurring.

The thing you mentioned about container interactivity is giving me a lot of pause, too. I have a few ideas as far as that goes, but they're pretty sketchy as of now. I'm putting a lot of this aside for now as I work on the program that reads the game-files and turns them into usable data. I've got the dialogue-tree system up and running, and I'm working on making the file-reader as kind and non-error prone as possible.

Which brings me to another question, concerning errors--am I better off setting it up so errors are accounted for when it comes to file-reading (exception error? ignore this chunk of data, move on), or better off making it cause a crash with an explanation as to a basic guess as to why it crashed? If the latter, how can I cause the program to abruptly stop and send a message saying why it stopped?

EDIT: Nevermind, I figured it out. And as I'm gutting and cutting the code, I'm starting to figure out where 'full stop, this is bad' is a good idea, and 'just ignore this, move on' is also a good idea.
User avatar
The Great Hippo
 
Posts: 5489
Joined: Fri Dec 14, 2007 4:43 am UTC
Location: behind you

Re: (Newbie Warning) Coding Rogue-Clone in Python

Postby Ben-oni » Wed May 16, 2012 1:13 am UTC

The Great Hippo wrote:Which brings me to another question, concerning errors--am I better off setting it up so errors are accounted for when it comes to file-reading (exception error? ignore this chunk of data, move on), or better off making it cause a crash with an explanation as to a basic guess as to why it crashed? If the latter, how can I cause the program to abruptly stop and send a message saying why it stopped?

EDIT: Nevermind, I figured it out. And as I'm gutting and cutting the code, I'm starting to figure out where 'full stop, this is bad' is a good idea, and 'just ignore this, move on' is also a good idea.

As you've probably figured out by now, there are two kinds of errors (well, two that I care about in this case): recoverable errors, and non-recoverable errors. The error is recoverable if your program can still perform its primary function. The standard way of dealing with both is to raise an exception, and only handle the kinds of exceptions that are recoverable. For instance, suppose you have a corrupted module you're trying to import into the game. Just crash? Probably not. The file reader should probably come to a full stop, though, and an error logged that explains a bit about what happened. The main program can continue, though, and just not load the file. Even within the file reader you might be able to ignore certain bad lines without failing completely. Use the same pattern of raising an exception, and recording the reason before moving on.
Ben-oni
 
Posts: 268
Joined: Mon Sep 26, 2011 4:56 am UTC

Re: (Newbie Warning) Coding Rogue-Clone in Python

Postby The Great Hippo » Wed May 16, 2012 6:10 am UTC

Thanks! You've been of extraordinary help!

I'm starting tomorrow on the type and subtype system (which will be handled as just another property, but it'll let the game auto-load properties based on a type--so FLOOR types will have a list of properties automatically set upon creation, streamlining the creation process, and also have code that prevents a FLOOR from ever ending up in your inventory or in a creature's container). Types will be set by a type file which will get loaded into the file reader, read all the properties set by type, then apply those properties to every object created of that type at the point of creation. It'll also help the engine figure out what to do with certain objects (like something as simple as checking to make sure there's a floor under the player's feet, and applying the fall function if there isn't).

Now that I'm starting to get deeper into this, I'm concerned that my code might be getting extraordinarily sloppy--I've always just coded by myself, so I'm not always familiar with what constitutes 'bad code' versus 'good code' (in terms of functionality, readability, and 'to the point-ness'). As I do this, I kind of want to make sure I hammer out any bad habits and develop good habits, since this is a project that's probably going to take (ball-park estimate) about a year or so to finish.

I'm probably going to start posting snippets of my code here and asking for advice on how I can make it more 'emblematic' of 'good code'--if that's alright. Here's one example:
Spoiler:
Code: Select all
    def move(self, dx, dy, dz):
        #check to make sure object has x-y-z properties
        if 'x' in self.properties and 'y' in self.properties and 'z' in self.properties:
            x, y, z = self.properties['x'], self.properties['y'], self.properties['z']
            destx, desty, destz = x + dx, y + dy, z + dz
            #check to make sure destination coordinates are not off the map
            if destx >= MAP_WIDTH or destx <= 0:
                dx = 0
            if desty >= MAP_HEIGHT or desty <= 0:
                dy = 0
            if destz <= 0:
                dz = 0
            #collision detection
            for object in objects:
                if 'x' in object.properties and 'y' in object.properties and 'z' in object.properties:
                    if object.properties['x'] == destx and object.properties['y'] == desty and object.properties['z'] == destz:
                        #eventual 'on-touch' code
                        if 'blocked' in object.properties and 'blocked' in self.properties:
                            if object.properties['blocked']:
                                if self.properties['blocked']:
                                    dx, dy, dz = 0, 0, 0
            self.properties['x'] += dx
            self.properties['y'] += dy
            self.properties['z'] += dz

This is code that handles object movement. It accepts coordinates: 0, -1, 0 to move one north, for instance. Then retrieves the destination of those coordinates, figures out if that destination is off-map (right now anything above 0 is off map for the z coordinate, since I'm not messing with that), and figures out if that destination contains any objects that block movement. Then it applies the given coordinates (modified to 0 if movement shouldn't be allowed) to the x-y-z properties of the given object.

I'm worried this code is going to get pretty large, because movement is an important function in-game, and I'm going to have to add a lot more code--things like floor detection (is there a floor on the new location? no? then fall), on-touch triggers (if you touch x, cause y), and context sensitivity commands (if you collide with a monster, you should attack it).
User avatar
The Great Hippo
 
Posts: 5489
Joined: Fri Dec 14, 2007 4:43 am UTC
Location: behind you

Re: (Newbie Warning) Coding Rogue-Clone in Python

Postby Ben-oni » Wed May 16, 2012 7:28 pm UTC

First, make your life easier and define the following:
Code: Select all
def hasPropVal(obj, prop, *args):
   '''checks for the existence of a property, whether it's in a set of values, or if it satisfies a given predicate'''
   if prop not in obj.properties:
      return False
   if len(args) == 0:
      return True
   if len(args) == 1 and callable(args[0]):
      return args[0](obj.properties[prop])
   return obj.properties[prop] in args
Use this religiously, like so:
Code: Select all
hasPropVal(obj, 'blocked', True) # determines if obj is blocking
hasPropVal(obj, 'x') # determines if obj has an 'x' value
hasPropVal(obj, 'type', 'Floor', 'Wall') # determines if obj is a Floor or Wall
hasPropVal(obj, 'y', lambda(y): y>0 and y < MAX_HEIGHT) # determines if obj has a valid y coordinate


Second, use inner functions to abstract from representation*.
Code: Select all
def move(self, dx, dy, dz):
   def canMove():
      return hasPropVal(self, 'x') and \
         hasPropVal(self, 'y') and \
         hasPropVal(self, 'z')
   def getPosition(obj):
      return (obj.properties['x'], obj.properties['y'], obj.properties['z'])
   def isValidLocation(x, y, z):
      return x < MAP_WIDTH and x > 0 and \
             y < MAP_HEIGHT and y > 0 and \
             z > 0

While not particularly useful in and of themselves (they'll only be called once), they separate the representation of position from the rest of the code. Move on to book-keeping:

Code: Select all
   if dx == 0 and dy == 0 and dz == 0:
      return
   if not canMove():
      raise Exception('Cannot Move')
   (x, y, z) = getPosition(self)
   x, y, z = x+dx, y+dy, z+dz
   if not isValidLocation(x, y, z):
      raise Exception('Out of Bounds')


Easy stuff. Just make sure you subclass Exception and return the correct kind instead of relying on strings as symbols. Now for the good stuff:

Code: Select all
   # Objects already in the target location
   objs = filter(lambda(obj): obj != self and \
               hasPropVal(obj, 'x', x) and \
               hasPropVal(obj, 'y', y) and \
               hasPropVal(obj, 'z', z), objects)
   
   if hasPropVal(self, 'blocked', True):
      # Objects that 'block'
      blocking = filter(lambda(obj): hasPropVal(obj, 'blocked, True), objs)
      if len(blocking) > 0:
         raise Exception('Blocked', *blocking)

'filter' is a handy little tool: use it often. Instead of iterating in a for loop, we can just filter for whatever has the desired properties. The "obj != self" is a bit pedantic, since we've already made sure the new location is different, but there will likely arise situations where it's important. Also note that any blocking objects are given as parameters when the Exception is raised, so calling code can see just why the move didn't happen, and not have to perform redundant work to figure it out**. And to wrap up:

Code: Select all
   for obj in objs:
      # interact
   
   self.properties['x'] = x
   self.properties['y'] = y
   self.properties['z'] = z


Every concern you're worried about can be split fairly orthogonally from the rest. For instance, you mentioned testing for a floor and falling if it's not there. Maybe try this:
Code: Select all
checkFloor(move):
   def moveAndFall(self, dx, dy, dz):
      def onFloor():
         # test for floor
      def fall():
         move(self, 0, 0, -1)
      move(self, dx, dy, dz)
      try:
         while True:
            fall()
      except Exception as e:
         if e[0] != 'Blocked':
            raise e
   return moveAndFall

And then
Code: Select all
@checkFloor
def move(self, dx, dy, dz):


Note that if there's no floor, the OutOfBounds exception will pass through to the calling function. (Of course, you should be more careful and check that the only blocking object was the floor, but that's pretty easy.) Writing a similar decorator to check for blocking monsters would work fairly similarly. Just make sure the decorators commute, or you'll end up with a headache.



*paraphrased from The Little Schemer's Eighth Commandment: "Use help functions to abstract from representation." The Ninth Commandment is also applicable: "Abstract common patterns with a new function."

**This is the Tenth Commandment, "Build functions to collect more than one value at a time."
Ben-oni
 
Posts: 268
Joined: Mon Sep 26, 2011 4:56 am UTC

Re: (Newbie Warning) Coding Rogue-Clone in Python

Postby The Great Hippo » Wed May 16, 2012 10:52 pm UTC

You've given me a tremendous amount of stuff to look up and read about, which is fantastic, because I can already see how pretty much everything here applies to my code--I'm definitely going to end up rewriting the whole thing with a lot of this in mind (I didn't know you could put definitions inside of definitions, for instance, and filter seems crazy useful and will get rid of so many wasteful loops I have--I can see a lot of places where *args will let me collapse multiple functions into one function--and I didn't know you could put multiple booleans in a single if-then statement and cause it to not fire if even one of those booleans came out false--this is going to make lots of my code much simpler and cleaner!).

I'm reading up more on syntax now and getting a better understanding of what's going on, but I wanted to ask a few quick questions, because while a lot of the documentation I'm relying on helps, there are a few details about your code that elude me, and it's hard to test them to figure out what they do until I understand how to call them:

* That first bit of code alone (with *args) cuts so much redundant code (particularly with how it does double-duty--allowing it to check if a property is present, and additionally if that property has a certain value!). Just from reading this bit alone, I think I've learned how to write much better, more powerful functions. But one chunk is throwing me about it--what is len(args) == 1: checking for? And I can't puzzle out what it's returning, either--obviously a boolean value, but I'm not sure how it's getting there or what it's measuring. Of the four statements you provided, is one of them an example of something that would hit this segment of code?

* I'm learning more about defining Exception subclasses now, and it's easy enough to write code that tests what they can and can't do, and I can see how extensive error-handling is going to pay off enormous dividends later down the road--I just wanted to double check, though--the 'raise Exception' argument is always intended to (outside of try: / except:) close the program with an error, right? So, in the example you gave, once replaced with calls to the exception subclasses, those would perform a full stop and give the custom error that would explain what caused the crash and (hopefully) why? Is there any way (or reason) to create Exception subclasses that don't crash the program when raised? Or is that essentially counter to their purpose?

(I beg your pardon for treating you as something of a Python tutor; there's tons of documentation for me to read out there, but without anyone to talk to about it, I lose a lot of the potential power and utility of the code I'm reading about--also, the few places I checked before coming here seem pretty hostile to newbie questions, and pretty prone to responding to them with 'go check the documentation')
User avatar
The Great Hippo
 
Posts: 5489
Joined: Fri Dec 14, 2007 4:43 am UTC
Location: behind you

Re: (Newbie Warning) Coding Rogue-Clone in Python

Postby Ben-oni » Thu May 17, 2012 1:19 am UTC

The Great Hippo wrote:You've given me a tremendous amount of stuff to look up and read about, which is fantastic, because I can already see how pretty much everything here applies to my code--I'm definitely going to end up rewriting the whole thing with a lot of this in mind (I didn't know you could put definitions inside of definitions, for instance, and filter seems crazy useful and will get rid of so many wasteful loops I have--I can see a lot of places where *args will let me collapse multiple functions into one function--and I didn't know you could put multiple booleans in a single if-then statement and cause it to not fire if even one of those booleans came out false--this is going to make lots of my code much simpler and cleaner!).

When you have the time, check out "map", too. And while you're at it, list comprehensions, which combine both map and filter with some syntactic sugar.

* That first bit of code alone (with *args) cuts so much redundant code (particularly with how it does double-duty--allowing it to check if a property is present, and additionally if that property has a certain value!). Just from reading this bit alone, I think I've learned how to write much better, more powerful functions. But one chunk is throwing me about it--what is len(args) == 1: checking for? And I can't puzzle out what it's returning, either--obviously a boolean value, but I'm not sure how it's getting there or what it's measuring. Of the four statements you provided, is one of them an example of something that would hit this segment of code?

"args" is a tuple of the rest of the function parameters. That means it has length. "len(args)==0" asks if there are no additional parameters. Then, naturally, "len(args)==1" asks if there is exactly one parameter. I've then gone on to ask if that parameter ("args[0]") is "callable". That is, is it a function that can be called with a parameter? So,
Code: Select all
hasPropVal(obj, 'y', lambda(y): y>0 and y < MAX_HEIGHT)
triggers that piece of code.

Note that "lambda" forms a callable function without giving it a name: an anonymous function, if you will. Someday (hopefully soon) lambda will be your bestest friend ever. Python's implementation of lambda is kinda pathetic, though, so you may find yourself using named functions anyways.

* I'm learning more about defining Exception subclasses now, and it's easy enough to write code that tests what they can and can't do, and I can see how extensive error-handling is going to pay off enormous dividends later down the road--I just wanted to double check, though--the 'raise Exception' argument is always intended to (outside of try: / except:) close the program with an error, right? So, in the example you gave, once replaced with calls to the exception subclasses, those would perform a full stop and give the custom error that would explain what caused the crash and (hopefully) why? Is there any way (or reason) to create Exception subclasses that don't crash the program when raised? Or is that essentially counter to their purpose?

That would kinda defeat the purpose. Raising an exception gets you out of the calling function, all the way up the stack to where it can be handled. If an uncaught exception didn't terminate the program, where would execution continue? So catch your exceptions!

With that in mind, you should document what kinds of exceptions any given function can raise, so that calling functions know to take care of them. If they can't, then they should be marked as potentially raising those exceptions as well. In some cases you might use exceptions when you want to make an assertion. In that case, program termination is usually fine, so it doesn't need to be indicated. Languages like Java make exceptions part of the type system, so programs won't compile if there are uncaught exceptions. Python is more flexible.

Note that in threaded scenarios, uncaught exceptions might not crash the program. Instead, they'll just quietly terminate the thread. This may or may not be a good thing.


Note that Python is untyped. Use this to your advantage. The example "hasPropVal", for instance, uses this to do double duty: check the value of a property, or check to see if it satisfies a given predicate. It depends on what parameters you pass in. But this means that you can call a function inappropriately, too, for instance, pass a string where a number was expected. This doesn't necessarily fail right away, but can cause problems down the line: you might end up sorting a number into a list of strings. Take note: a function doesn't need to check to make sure it's being used correctly. That's the caller's responsibility. This is one point where Exceptions come up. If you use something incorrectly, it's your responsibility to take care of the exceptions.
Code: Select all
def hasPropVal(obj, prop, val):
   try:
      return obj.properties[prop] == val
   except Exception as e:
      return False

As they say, it's easier to ask forgiveness than it is to ask permission. If a function accepts objects that have a certain field, just assume they have it. If they don't, let the exception be thrown.

(I beg your pardon for treating you as something of a Python tutor; there's tons of documentation for me to read out there, but without anyone to talk to about it, I lose a lot of the potential power and utility of the code I'm reading about--also, the few places I checked before coming here seem pretty hostile to newbie questions, and pretty prone to responding to them with 'go check the documentation')

Oh, don't worry. I have less Python experience than you. Three lectures worth, actually, and I skipped all three. I just know where to find the interpreter in Terminal. What really matters is programming language experience. The more you know, the better off you'll be.
Ben-oni
 
Posts: 268
Joined: Mon Sep 26, 2011 4:56 am UTC

Re: (Newbie Warning) Coding Rogue-Clone in Python

Postby The Great Hippo » Thu May 17, 2012 5:11 pm UTC

Just to update--I might have some questions later, but at the moment I'm just fixing code with the information you gave me, and I wanted to demonstrate how much it's helping. No need to read if you don't feel like it, but I thought you might want to see how much better (at least, I hope it's better!) my code is looking now that I've taken your advise in mind.

Here's some of the original functions for my file parser (it breaks a given string apart into usable chunks, then interprets them). Spoilered for awful:
Spoiler:
Code: Select all
#returns first string found between symbol_1 and symbol_2; if return_values
#is True, also returns symbol_1 and symbol_2
def slice(text, symbol_1, symbol_2, return_values = False):
   #if '*' present, interpret with wildcard code
   if symbol_1.find('*') > 0 or symbol_2.find('*') > 0:
      return wildcard(text, symbol_1, symbol_2, return_values)
   #checks to make sure symbols are present in text
   if text.find(symbol_1) == -1 or text.find(symbol_2) == -1:
      return ""
   #checks to make sure symbol 1 does not occur /after/ symbol 2
   if text.find(symbol_2) < text.find(symbol_1):
      return ""
   text = text[(text.find(symbol_1) + len(symbol_1)):text.find(symbol_2)]
   if return_values == True:
      text = symbol_1 + text + symbol_2
   return text

Code: Select all
def wildcard(text, symbol_1, symbol_2, return_values):
   #split symbol_1 and two into two parts (on each side of the asterisk)
   symbol_1 = split(symbol_1, "*")
   symbol_2 = split(symbol_2, "*")
   #first, does the first part of symbol_1 occur in the text?
   if text.find(symbol_1[0]) < 0:
      return ""
   #mark that position.
   start_1 = text.find(symbol_1[0])
   #next, does the second part of symbol_1 occur in the text *after*
   #the first part?
   if text[start_1:].find(symbol_1[1]) < 0:
      return ""
   #mark that position.
   end_1 = text[start_1:].find(symbol_1[1]) + start_1 + 1
   #does the first part of symbol_2 occur in the text *after* symbol_1?
   if text[end_1:].find(symbol_2[0]) < 0:
      return ""
   #mark that position.
   start_2 = text[end_1:].find(symbol_2[0]) + end_1
   #next, does the second part of symbol_2 occur *after* the first part?
   if text[start_2:].find(symbol_2[1]) < 0:
      return ""
   #mark that position.
   end_2 = text[start_2:].find(symbol_1[1]) + start_2 + 1
   #finally, determine if we are returning the string with or without
   #the values given to the function.
   if return_values == False:
      return text[end_1:start_2]
   else:
      return text[start_1:end_2]


Code: Select all
def split(text, symbol = "="):
   #checks to make sure an equal sign occurs, otherwise returns list with text and empty string.
   value = [text, ""]
   if text.find(symbol) == -1:
      return value
   #turns our string into a list containing two strings.
   value = [text[:text.find(symbol)], text[text.find(symbol) + 1:]]
   return value

The wildcard function, in retrospect, is particularly horrifying (I had to comment every line just so I could keep track of what I was doing--it took me hours to finally work it out, and I think the comments actually outnumber the actual code). Now, using the style you suggested:
Spoiler:
Code: Select all
def hasString(text, str1, str2 = ""):
   if str2 == "":
      return text.count(str1) > 0
   return text.count(str1) > 0 and text.count(str2) > 0 and \
         text.find(str1) < text.find(str2)

Code: Select all
def splitString(text, symbol = "="):
   if not hasString(text, symbol):
      return [text, ""]
   return [text[:text.find(symbol)], text[text.find(symbol) + 1:]]

Code: Select all
def getString(text, str1, str2, return_strings = False):
   def wildcard(text, str1, str2):
      sym1, sym2 = splitString(str1, '*'), splitString(str2, '*')
      sym1, sym2 = getString(text, sym1[0], sym1[1], True), getString(text, sym2[0], sym2[1], True)
      return sym1, sym2
   if hasString(str1, "*") or hasString(str2, "*"):
      str1, str2 = wildcard(text, str1, str2)
   if not hasString(text, str1, str2):
      return ""
   result = text[(text.find(str1) + len(str1)):text.find(str2)]
   if return_strings == True:
      result = str1 + result + str2
   return result

Everything I did in the first bit is accomplished in the second bit with somewhere near half the code and way more elegance. The wildcard function alone has dropped down from 15+ lines to 3 lines, and it took me all of two minutes to figure out how to do it. And with hasString, I've collapsed the 'check for string' and 'check string x occurs before string y' thing into a single function; I wouldn't have thought about that before I read your suggested code changes.

I don't even need comments for this code; it's pretty much self-explaining (for my purposes, anyway). I'm sure there are even better ways I could do the things I'm doing here, but now that I'm approaching this in a way that emphasizes smaller, more numerous, more elegant functions, the code's looking a lot cleaner and easier to navigate. Thanks!

EDIT: Just noticed a minor glitch; because the getString function returns an empty string when it doesn't find the submitted string, the wildcard function would turn one of the strings into an empty string, then resubmit it--resulting in 'shenanigans'--but this was fixed easily enough by just adding 'or str1 == "" or str2 == ""' to the 'hasString()' check, which then causes the function to return an empty string.
User avatar
The Great Hippo
 
Posts: 5489
Joined: Fri Dec 14, 2007 4:43 am UTC
Location: behind you

Re: (Newbie Warning) Coding Rogue-Clone in Python

Postby Ben-oni » Thu May 17, 2012 7:30 pm UTC

I know it's a definite improvement, but some of those lines made me want to cry, so I've made modifications:

Code: Select all
def splitString(text, symbol = "="):
   return (text[:text.find(symbol)], text[text.find(symbol) + len(symbol):]) \
      if hasString(text, symbol) else (text, "")

def getString(text, str1, str2, return_strings = False):
   def wildcard(str): return getString(text, *splitString(str, '*'), return_strings = True)
   
   sym1 = wildcard(str1) if hasString(str1, "*") else str1
   sym2 = wildcard(str2) if hasString(str2, "*") else str2
   if not hasString(text, sym1, sym2):
      return ""
   result = text[(text.find(sym1) + len(sym1)):text.find(sym2)]
   return sym1 + result + sym2 if return_strings else result

The modification to splitString is just to return a tuple instead of a list, which is needed for a simplification to wildcard, which is now a single line. And that's what I was really cringing at. You performed exactly the same operation on two parameters, each independent of the other. Why not just call the function twice? Also, the bit about assigning the variables, and then straightway returning them made me die a little.

Some things you might not have been aware of: inner functions have access to variables in the outer function's scope, so there was no need to pass text into wildcard because it already knows about it. Using a parameter name within a function that is the same as a parameter in an outer scope is called "shadowing", and some debuggers will give you a warning about it. Also, you can pass a variable number of arguments into a function with tuples, even if the function calls for a fixed number of arguments. Third, there's a trinary conditional operator in most languages. In python, it's "<expr1> if <boolean expr> else <expr2>". It's useful. Use it. (I admit, using it for splitString may have made the function less clear; I just wanted to get it all onto one line so you could see what "\" really does.)

A stylistic concern: don't modify variables outside of loops. Use a new symbol, even if you won't need the old one any more. Mutation causes errors. By making the change explicit, you reduce the chance of error creeping in.



One of these days you'll learn about parsing for real. There are some great libraries out there (I like Parsec, myself), but you'll have to really understand the topic before you can take advantage of them. Meanwhile, try to keep your data formats as simple as possible so that you don't waste time with parsing. However, most of what you want here can probably be accomplished with regular expressions. They're easy to learn and easy to use, and can reduce everything to a single line. Python's implementation is... kinda gimped.

Code: Select all
import re

def getString(text, str1, str2):
   def escape(str): return re.sub(r"\\\*", ".*?", re.escape(str))
   return re.search(escape(str1) + "(.*?)" + escape(str2), target).group(1)

If I understood your code right, this looks for str1 and str2 in text, and grabs whatever is between them. This also corrects an error in your code. What happens if str1 contains str2? Also, note that if nothing is found, getString raises an exception instead of returning "". This is because the empty string ("") is a valid response, whereas not finding str1 or str2 (or finding them in the wrong order) means the search was invalid. So we can differentiate between the two by raising an exception.

Note that Regular Expressions is a language in and of itself, and not specific to Python. You can ask just about anyone for help with a regex problem.
Ben-oni
 
Posts: 268
Joined: Mon Sep 26, 2011 4:56 am UTC

Re: (Newbie Warning) Coding Rogue-Clone in Python

Postby The Great Hippo » Fri May 18, 2012 6:12 am UTC

Wow, thanks!--I'm getting the sense that this is going to be an ongoing trend--where I find myself going 'oh cool, I fixed this with an awesome solution!'--only to discover that there's a solution about 10 times better right down the road! Pardon if my previous post sounded a little arrogant, by the way; I was just tickled pink that I was seeing an immediate and enormous shrinkage of code.

I wanted to ask one question on the side: Is it a good programming practice to create single-line functions for parameters that are regularly called? For example, instead of writing 'obj.properties[property]' (or 'self.properties[property]') all the time, should I create a function called 'getProp()' that returns this value ('getProp(obj, property)')? The advantage I'm imagining is the ability to consolidate all my calls to obj.properties[property] to one place, so if I ever needed to change that reference (unlikely! but possible), I'd only have to change one line of code. But I'm wondering if single-line functions like that are actually a good practice protocol?

Also, the if-then-else single line expression is great (I had no idea the if-then was that flexible), and it'll probably see a ton of use in my code. I'm still working on the habit of thinking in terms of anonymous functions, but I imagine that'll soon come. And yeah, didn't realize that defined functions inside of functions inherit the variables of their parent function--again, thanks!
User avatar
The Great Hippo
 
Posts: 5489
Joined: Fri Dec 14, 2007 4:43 am UTC
Location: behind you

Re: (Newbie Warning) Coding Rogue-Clone in Python

Postby Ben-oni » Fri May 18, 2012 8:02 am UTC

Concerning single line functions... there's nothing inherently wrong with them. I use them all the time. I also rarely (never, actually) use Python, so they're a bit different for me.

In this case, there's a tradeoff, which is that you lose the "lvalue" of the expression by wrapping it in a function call. That is,
Code: Select all
object.properties['property'] = x # this is fine
getProp(object, 'property') = x # this is not fine

And honestly, both statements are about the same length, so you've gained very little. This is actually a case for setf, where you first describe how to retrieve the value, and then how to set the value. It's nifty stuff, but pretty specific to Common Lisp. You're not about to get that sort of behavior with Python. In C you might use preprocessor macros to do something like
Code: Select all
// Warning: definite pseudocode; don't try this at home
#define GET_PROP(obj, prop)     obj.properties[prop]
You've then retained the lvalue, and as long as you remember that GET_PROP is a macro, you're fine (which, after all, is why it's in caps). Of course, you'd never do that, because in C you can pass lvalues around... In Python, you'll probably want your objects to be of a class that defines "getProp" and "setProp" functions, so your syntax would become "object.getProp('property')" and "object.setProp('property', value)".

I wouldn't say that one method or another is right, or even stylistically better. Just be consistent with whatever you choose. I would recommend using a single line function when it gains you something substantive. Even if it's only used in one place, the abstraction and clarity can be considered worthwhile. In case you were interested, here's some code I wrote some time ago that illustrates single line functions. It's date-stamped 5/25/09.
Code: Select all
rotate (a:as) = as ++ [a]
rotations as = take (length as) $ iterate rotate as
hasCircularProp n p = all (p . read) $ rotations $ show n

It's not Python, but it should be pretty clear: rotate moves the front of a list to the back, rotations generates all rotations of a list, and hasCircularProp tells if all the rotations of a number share a given property. Of course, I could have made it a single massive line...
Code: Select all
hasCircularProp n p = all (p . read) $ (\str -> take (length as) $ iterate (\as -> tail as ++ [head as]) str) $ show n
but that would be fairly illegible. Anyways, you should be getting the idea by now. Clarity and simplicity are prized; when short functions achieve that, use them. Otherwise, do whatever is easiest to read. Genuinely complex algorithms probably shouldn't be reduced beyond your ability to grasp their function. For instance, "powSet = filterM $ const [True, False]", while a valid and concise definition of the power set, is probably a bad idea*.



For those interested, powSet is using a nondeterministic filter. The predicate ignores the value being tested, and returns both True and False, so filterM branches both possibilities. That, by the way, is not what filterM does. It's far more interesting than just performing rudimentary nondeterminism.
Ben-oni
 
Posts: 268
Joined: Mon Sep 26, 2011 4:56 am UTC

Re: (Newbie Warning) Coding Rogue-Clone in Python

Postby Yakk » Fri May 18, 2012 7:32 pm UTC

Also, the bit about assigning the variables, and then straightway returning them made me die a little.
I find that highly useful when stepping through a function in a debugger. The "function returned" automatic tracking is more awkward to use, and it is harder to "back up" and repeat how it was generated if the result is anomalous, as well as harder to set up conditional breakpoints.

In general, doing code in fewer lines is fun for stunting, but it tends to have little optimization advantages, and doesn't make the code easier to understand.

On the other hand, practicing doing that kind of thing exercises your understanding of the language.

Remember, code that don't need comments immediately after you write it is not code that doesn't need comments. Comments are for the people who don't already know what the function is doing, including yourself in a month.

---

The next thing I'd think about is your architecture. You are building a graph, so take a page from graph theory -- the Edge is as important as the Node.

And instead of one graph, have more than one graph, with nodes identified. So the physical graph can be distinct from the character experience graph, and you don't have the problem that a character "having a level of fighter" is distinct from a character "having a sword" in what kind of operations are allowed.

Your character "physically has a sword" while "build has a level of fighter". Both are has-a relationships.

I guess this can get tricky, distinguishing between equipped items and items you carry. I suppose some items you physically have might be body parts. :) You "have a" hand, which "has a" sword in it. The sword modifies the hand, which modifies the character. But that is probably a path down which madness lies.

As an example, you can restrict physical objects in how they enter and exit other physical object's "possession". A physical object has a context (the thing that possesses it), and one can only enter the possession of another object which shares your context (and is not yourself) (you can only enter a bag if you are in the same room as the bag), or whose context is the context of your context (ie, you can leave a bag).

If you enforce that rule, then bag-inside-itself is impossible.

Now, teleportation can violate this if it doesn't rely on an overlying context. But that just means whenever you teleport you have to be careful, and otherwise the problem doesn't exist.

Another nice thing about this trick is that when (say) a gernade explodes, it can figure out where it is in order to damage its environment. And a bag can list its contents.

...

I'd also be tempted to build my own OO system involving objects-as-instances, objects-as-classes, objects-as-meta-classes, and a messaging system. I wouldn't advise you to do this...
Spoiler:
Each object has a class, which is an object. The basic (public) interface is "can I get a handler for this message" and "execute this message with this handler" and "can you produce a handler for this message on me?" which is distinct from sending a message directly (a class can handle messages to itself distinctly from messages that where directed at an instance of the class). Possibly I'd augment this with pre-handlers and post-handlers, whereby the instance asks the class for a pre and post-handler for each message.

When an object is sent a message, and the message isn't processed by the object, it asks its class if it has a handler for it, passing along a reference to itself.

My default class would have a meta-class of inheritance. The inheritance meta-class, when it was asked for a handler from a class, would query the class for parents, and see if any of them can handle the message.

Construction is a message you send to a class to produce an instance. The inheritance meta-class construction handler takes a list of parent classes, and builds the class instance (with no handlers in it by default). It builds a construction pre-handler for the class that builds the instance class heirarchy mayhap.

But I'd recommend against doing this yourself -- I'd be just playing around. :) I've never built an OO system myself, and it would be an amusing way to learn something while writing a Roguelike.
One of the painful things about our time is that those who feel certainty are stupid, and those with any imagination and understanding are filled with doubt and indecision - BR

Last edited by JHVH on Fri Oct 23, 4004 BCE 6:17 pm, edited 6 times in total.
User avatar
Yakk
 
Posts: 10038
Joined: Sat Jan 27, 2007 7:27 pm UTC
Location: E pur si muove

Re: (Newbie Warning) Coding Rogue-Clone in Python

Postby Ben-oni » Fri May 18, 2012 7:56 pm UTC

Yakk wrote:Each object has a class, which is an object. The basic (public) interface is "can I get a handler for this message" and "execute this message with this handler" and "can you produce a handler for this message on me?" which is distinct from sending a message directly (a class can handle messages to itself distinctly from messages that where directed at an instance of the class). Possibly I'd augment this with pre-handlers and post-handlers, whereby the instance asks the class for a pre and post-handler for each message.

When an object is sent a message, and the message isn't processed by the object, it asks its class if it has a handler for it, passing along a reference to itself.

My default class would have a meta-class of inheritance. The inheritance meta-class, when it was asked for a handler from a class, would query the class for parents, and see if any of them can handle the message.

Construction is a message you send to a class to produce an instance. The inheritance meta-class construction handler takes a list of parent classes, and builds the class instance (with no handlers in it by default). It builds a construction pre-handler for the class that builds the instance class heirarchy mayhap.

I've toyed with this idea a bit, but chose not to offer it. While the OP certainly appears to be reinventing object systems, it's hard to say without knowing the architecture what would work best. What you've described here sounds like a messaging/delegate system. In such a scenario, you can extend the interface of an object by giving it a new delegate that can handle more messages, but it doesn't really solve the underlying problem: how do those delegates interact with each other? In this situation, my feeling was that it's better to show the OP a few new tools and let him discover how to do things on his own. He'll have a number of revelations on his own as he solves the problems, and when he runs into trouble he doesn't know how to deal with, we'll get a chance to really exercise our creative juices.
Ben-oni
 
Posts: 268
Joined: Mon Sep 26, 2011 4:56 am UTC

Re: (Newbie Warning) Coding Rogue-Clone in Python

Postby The Great Hippo » Fri May 18, 2012 10:34 pm UTC

Yakk wrote:The next thing I'd think about is your architecture. You are building a graph, so take a page from graph theory -- the Edge is as important as the Node.

And instead of one graph, have more than one graph, with nodes identified. So the physical graph can be distinct from the character experience graph, and you don't have the problem that a character "having a level of fighter" is distinct from a character "having a sword" in what kind of operations are allowed.

Your character "physically has a sword" while "build has a level of fighter". Both are has-a relationships.
I'm rewriting the pseudo-parser from scratch; Ben-oni's posts helped me realize I've been trying to re-invent the wheel--all it needs to do is read a text-file and assign the values inside it to a dictionary. My purpose for doing so is to create a loadable type system, but a very gently enforced one (so I can have really weird things happen if I want to, but not if I don't want to). The text file will have entries for multiple types, import them in multiple dictionaries (kept in a master dictionary called 'Type'), import it into the game--and then whenever an object is created, the first thing that the code will look at will be its type property (error if an object is untyped), after which it will assign all the properties under that type to the object.

The 'base types' are going to be the ones that can't really mix--that are treated differently on the code-level. So, say, 'Wall' is a type, and one of the rules about 'Wall' typed objects is they can't be put inside of an object's container. 'Status' might be a type, and the code won't let them exist outside containers. After that, subtypes define properties more specifically (hostile enemies, friendly people, steel walls, rock walls), and sub-subtypes get even more specific--but subtypes and sub-subtypes aren't treated any differently by the code, they're just there to make setting properties easier.

Is this what you mean by multiple graphs? It would mean a simple rule that prevents, say, a fighter from beating you to death with a level of mage (but it would allow that to happen if I really wanted it to happen, which I might--one of the reasons this idea intrigued me is because of strange possibilities like this).

(Also, one of the things I want to do is keep Types as few and broad as possible, so I can have as many interesting interactions as possible! But obviously, some things cannot be allowed to interact with others)
Yakk wrote:I guess this can get tricky, distinguishing between equipped items and items you carry. I suppose some items you physically have might be body parts. :D You "have a" hand, which "has a" sword in it. The sword modifies the hand, which modifies the character. But that is probably a path down which madness lies.
...I'm sorry, but that's a fantastic idea and I can't imagine not wanting to do it regardless of what madness lies beyond. Just creating a system that treats body parts as added functionality for their holder (anything that has an arm can attack you and manipulate objects; anything with eyes can see you; anything with a mouth can talk to you) creates so many interesting consequences, particularly when bizarro magic enters the mix (walls that grapple, swords that chatter, floating eyeballs that relay their FOV to you, losing your arm and grafting it to your minion so he's got three). It would probably be tricky, because it involves intersecting rulesets (bodyparts are a container when they're 'in use' and an inventory object when they've been hacked off), but it's definitely interesting.

Of course, that's probably a late-game concern, not an immediate one.
Yakk wrote:As an example, you can restrict physical objects in how they enter and exit other physical object's "possession". A physical object has a context (the thing that possesses it), and one can only enter the possession of another object which shares your context (and is not yourself) (you can only enter a bag if you are in the same room as the bag), or whose context is the context of your context (ie, you can leave a bag).

If you enforce that rule, then bag-inside-itself is impossible.

Now, teleportation can violate this if it doesn't rely on an overlying context. But that just means whenever you teleport you have to be careful, and otherwise the problem doesn't exist.

Another nice thing about this trick is that when (say) a gernade explodes, it can figure out where it is in order to damage its environment. And a bag can list its contents.
One of the immediate things that occurs to me here is that floors would just be overglorified bags, or 'open chests' that you can walk on; players and creatures and such are just contained within their inventory (and if you're in its inventory, you're free to move objects in its inventory to your inventory). There'd have to be rulesets about allowing only one creature to occupy a floor's inventory space at a time, though (already handled by blocking, but teleportation... oh, wait, was that what you meant by teleportation creating a problem? Yeah, I see what you mean, then!)
Yakk wrote:
Spoiler:
Each object has a class, which is an object. The basic (public) interface is "can I get a handler for this message" and "execute this message with this handler" and "can you produce a handler for this message on me?" which is distinct from sending a message directly (a class can handle messages to itself distinctly from messages that where directed at an instance of the class). Possibly I'd augment this with pre-handlers and post-handlers, whereby the instance asks the class for a pre and post-handler for each message.

When an object is sent a message, and the message isn't processed by the object, it asks its class if it has a handler for it, passing along a reference to itself.

My default class would have a meta-class of inheritance. The inheritance meta-class, when it was asked for a handler from a class, would query the class for parents, and see if any of them can handle the message.

Construction is a message you send to a class to produce an instance. The inheritance meta-class construction handler takes a list of parent classes, and builds the class instance (with no handlers in it by default). It builds a construction pre-handler for the class that builds the instance class heirarchy mayhap.

But I'd recommend against doing this yourself -- I'd be just playing around. :) I've never built an OO system myself, and it would be an amusing way to learn something while writing a Roguelike.
Yeah, those are definitely words you've typed there. And they're arranged in sentences!

(obviously, one of my goals for doing this is to get to a point where things like the above don't sound like VCR instructions translated from Swahili by Babelfish)
User avatar
The Great Hippo
 
Posts: 5489
Joined: Fri Dec 14, 2007 4:43 am UTC
Location: behind you

Re: (Newbie Warning) Coding Rogue-Clone in Python

Postby Ben-oni » Fri May 18, 2012 11:28 pm UTC

The Great Hippo wrote:Of course, that's probably a late-game concern, not an immediate one.
Yakk wrote:As an example, you can restrict physical objects in how they enter and exit other physical object's "possession". A physical object has a context (the thing that possesses it), and one can only enter the possession of another object which shares your context (and is not yourself) (you can only enter a bag if you are in the same room as the bag), or whose context is the context of your context (ie, you can leave a bag).

These are not late game concerns. You should decide now what should be allowed and what must not be allowed. If you don't, you'll box yourself into a corner and find it incredibly hard to get out.

In other news, it sounds like you're trying to create some kind of DSL (Domain Specific Language) for writing game modules. I advise against this. You've chosen Python as your language, and any language you create is likely to be less useful. (Once you understand more about the limitations of Python and what it can't do, this will gradually cease to be true. You'll know when it's time.)

Instead of creating modules in a text file and parsing them, why not write them as Python modules, and import them? (For production purposes this is a terrible idea, because of security risks, but for now it should be fine.) The bonus here is that you can use Python's language to streamline the process of defining objects. A particular module might need to define three NPC sisters, each identical except for the dialog associated with each one. So the module could define one, and then use Python to copy and modify it. You then don't need to write any specialized instructions into the data format that explains the process.
Ben-oni
 
Posts: 268
Joined: Mon Sep 26, 2011 4:56 am UTC

Re: (Newbie Warning) Coding Rogue-Clone in Python

Postby The Great Hippo » Fri May 18, 2012 11:42 pm UTC

Oh, right--beg pardon! I completely forgot that I could load variables into python using modules with the "From X import Y" statement. That actually makes this much easier, as I don't even need a pseudo-parser anymore. I mainly needed a type library so I could start building rulesets about types and figuring out how the code is going to treat them.

Also, yeah, pardon about the body-part thing--shortly after I wrote that and went to work on the type library, I realized that if I decide to include it, it has to be in the type library, and I need to include rulesets for that specific type (otherwise, adding it later is going to be next to impossible). So for now, I'll add it (I'm thinking it'll be easier to take it out--or collapse its function into something else--then to add it in later on).
User avatar
The Great Hippo
 
Posts: 5489
Joined: Fri Dec 14, 2007 4:43 am UTC
Location: behind you

Re: (Newbie Warning) Coding Rogue-Clone in Python

Postby Ben-oni » Sat May 19, 2012 6:02 am UTC

The Great Hippo wrote:I'm thinking it'll be easier to take it out--or collapse its function into something else--then to add it in later on.

Oh so true. By the way, how are you implementing rulesets?
Ben-oni
 
Posts: 268
Joined: Mon Sep 26, 2011 4:56 am UTC

Re: (Newbie Warning) Coding Rogue-Clone in Python

Postby The Great Hippo » Sat May 19, 2012 3:43 pm UTC

Ben-oni wrote:Oh so true. By the way, how are you implementing rulesets?
I've been giving this a lot of thought at work (I do a graveyard shift, and by the time I get home I'm usually too exhausted to code, so I spend a lot of time thinking), and came to some conclusions that probably means a brief (hopefully last!) rewrite. It's going to be a little ranty, so I apologize in advance--I'm assembling my thoughts about the architecture in text for the first time.

First off, I suspect I've been thinking too literally about types. Why do walls have to be a separate type from creatures? They're just creatures that block sight, movement, never move, and are essentially indestructible. That's all stuff I can handle with sub-types and property distinctions. So, I don't think I need a Wall type.

Then I got to thinking about Floor types and how they work. Each floor is an object with an x-y-z coordinate; when an object that has 'blocked' true enters the floor's inventory, any other object with 'blocked' set to true cannot enter the floor's inventory. Also, the drawing function will just roll through all the floors on the map and check any objects in their inventory for characters to draw. Anything with 'blocked' set to true immediately cancels the drawing routine for that tile (so anything that blocks--walls and creatures, for example--won't be overwritten by things that don't block, like a dagger on the floor).

Something else occurred to me; if I went about things this way, it meant the move function would need a rehaul--what's more, since the floor has the x-y-z coordinate, and everything exists only in a floor's inventory (including walls), the only type of object that needs an x-y-z coordinate are floors. When anything else tries to move, a function will retrieve the floor that owns it, check the floor they're moving to (to see if it exists, and then check its inventory to see if something with 'blocked = True' is in it), then pass the actor from one inventory to another. I suspect this might be better than giving all objects their own x-y-z coordinate system and 'linking it up' with everything else, largely because it consolidates the most important, relevant data all in one place--the floor (if I'm completely wrong about this being a more efficient system, I'm definitely open to hearing about it; I'm not sure why this feels better to me, and I might just be interested in it because it's a weird--rather than an effective--solution).

So I'm left with two types--Floors and Things (descriptive, I know! But it includes everything that's on a floor--walls, creatures, items, containers!). Floors are unique because two floors can never share the same x-y-z coordinate, and because they can never be put inside of an inventory (walls have the same quality, but it's only because you won't get the opportunity--since they block, you can't step into the floor's inventory to pick them up), only hold things in their inventory. But that still doesn't cover things like HP, or poison, or a level in Fighter--so I need a third type, which can only exist inside of containers--'Effects'.

I need one more type, though--because if things can only exist on floors, how do I handle things like a cursor to select a spot on the screen? Or just a cool visual affect using ASCII graphics? Or a camera--what if, instead of always moving the screen with the player, I want a 'trailing camera' affect to follow them around on the screen? How would the camera navigate this labyrinthian ruleset I've got going here? How would just a plain cursor navigate it?

So, I guess I need a fourth set--one for objects that have absolutely zero in-game consequences--have their own x-y-z coordinate system independent of the floor tiles--and only exist to let me do things outside of the ruleset, so I can throw in some neat visual effects or let the player select a large area (or even just edit maps and place floor tiles if I want to go that route later on). I'm guessing I'll call it the 'System' type.

As for how the rulesets will be enforced--I'm thinking lists. Specifically, there's a master list called 'Objects' which includes every object in the game (I can't imagine why I'd need this list, or need to filter it, but who knows?), a list for each instance of an individual type (all floors, all things, all effects), and possibly some lists that 'overlap' a little, to make some of the processing easier (though I suspect this can create some serious problems later on if I'm not excruciatingly careful). So, filter the lists to get the relevant objects, apply whatever selectively to those objects.

Whew. Okay, I think that's everything.

(Actually, it isn't--body-parts! I gave that a lot of thought too--the problem is that it would be a type that can cross between other types--being a 'thing', or being an 'effect'. That's complicated, and dangerous, I imagine--so my solution was to think of them as an effect with a property that creates a 'thing' when a certain action is performed upon its owner--so, if you blow a dude up, and one of his effects is 'arm', then under certain conditions that effect will cause a thing called 'arm' to be produced on the ground. Which, under similar conditions, might 'disappear' and cause an effect called 'arm' to appear in someone or something else's container--basically, 'arm' as an effect and 'arm' as a thing are two totally different objects, linked together by a property)
User avatar
The Great Hippo
 
Posts: 5489
Joined: Fri Dec 14, 2007 4:43 am UTC
Location: behind you

Re: (Newbie Warning) Coding Rogue-Clone in Python

Postby Ben-oni » Sat May 19, 2012 8:02 pm UTC

Something else occurred to me; if I went about things this way, it meant the move function would need a rehaul--what's more, since the floor has the x-y-z coordinate, and everything exists only in a floor's inventory (including walls), the only type of object that needs an x-y-z coordinate are floors. When anything else tries to move, a function will retrieve the floor that owns it, check the floor they're moving to (to see if it exists, and then check its inventory to see if something with 'blocked = True' is in it), then pass the actor from one inventory to another. I suspect this might be better than giving all objects their own x-y-z coordinate system and 'linking it up' with everything else, largely because it consolidates the most important, relevant data all in one place--the floor (if I'm completely wrong about this being a more efficient system, I'm definitely open to hearing about it; I'm not sure why this feels better to me, and I might just be interested in it because it's a weird--rather than an effective--solution).

What about falling? Climbing?

I need one more type, though--because if things can only exist on floors, how do I handle things like a cursor to select a spot on the screen? Or just a cool visual affect using ASCII graphics? Or a camera--what if, instead of always moving the screen with the player, I want a 'trailing camera' affect to follow them around on the screen? How would the camera navigate this labyrinthian ruleset I've got going here? How would just a plain cursor navigate it?

So, I guess I need a fourth set--one for objects that have absolutely zero in-game consequences--have their own x-y-z coordinate system independent of the floor tiles--and only exist to let me do things outside of the ruleset, so I can throw in some neat visual effects or let the player select a large area (or even just edit maps and place floor tiles if I want to go that route later on). I'm guessing I'll call it the 'System' type.

Careful, now. Rendering should be handled separate from game logic. You don't want tight coupling there. Ask yourself this: do you want a game module to be able to change the basic rules for how the player interacts with the game? (You might, actually. For instance, there could be a mirror-of-seeing-things-that-are-far-away. There are already good solutions for this sort of extensibility problem, though.)

As for how the rulesets will be enforced--I'm thinking lists. Specifically, there's a master list called 'Objects' which includes every object in the game (I can't imagine why I'd need this list, or need to filter it, but who knows?), a list for each instance of an individual type (all floors, all things, all effects), and possibly some lists that 'overlap' a little, to make some of the processing easier (though I suspect this can create some serious problems later on if I'm not excruciatingly careful). So, filter the lists to get the relevant objects, apply whatever selectively to those objects.

But where do the rules go? Suppose I need to create a type of floor called a "trapdoor". How do I specify the rule that it's only triggered by "blocking" objects?

(Actually, it isn't--body-parts! I gave that a lot of thought too--the problem is that it would be a type that can cross between other types--being a 'thing', or being an 'effect'. That's complicated, and dangerous, I imagine--so my solution was to think of them as an effect with a property that creates a 'thing' when a certain action is performed upon its owner--so, if you blow a dude up, and one of his effects is 'arm', then under certain conditions that effect will cause a thing called 'arm' to be produced on the ground. Which, under similar conditions, might 'disappear' and cause an effect called 'arm' to appear in someone or something else's container--basically, 'arm' as an effect and 'arm' as a thing are two totally different objects, linked together by a property)

Why can't it be both an Effect and a Thing?
Ben-oni
 
Posts: 268
Joined: Mon Sep 26, 2011 4:56 am UTC

Re: (Newbie Warning) Coding Rogue-Clone in Python

Postby Robert'); DROP TABLE *; » Sat May 19, 2012 8:54 pm UTC

Ben-oni wrote:Why can't it be both an Effect and a Thing?

FYI, Python does allow one class to have two base classes. Assuming arm is a separate class, you can declare
Code: Select all
class Arm(Thing, Effect)

However, whether or not this is a good idea depends on what other code expects an Effect or Thing to have and do.

EDIT: (The OP's info, that is.)
...And that is how we know the Earth to be banana-shaped.
User avatar
Robert'); DROP TABLE *;
 
Posts: 633
Joined: Mon Sep 08, 2008 6:46 pm UTC
Location: in ur fieldz

Re: (Newbie Warning) Coding Rogue-Clone in Python

Postby The Great Hippo » Sun May 20, 2012 3:10 am UTC

Ben-oni wrote:What about falling? Climbing?
I'm thinking that falling occurs when you try to move to a space where there's no floor (walls are just floors with a 'wall' object on them, so this doesn't usually happen). You drop down a z-level until you hit a map where there is a floor. If you reach the very bottom of the max-map-limit and this doesn't happen, you're dead (we say you fell into the eternal void, or into a deadly crevice, and leave it at that). One problem with this though--what happens if a player falls on a square with a wall? I suppose I could design maps to account for this (never put a 'wall' at the bottom of a pit), but I'd like it to be more automated than that (whenever you place a wall, the z-level above it automatically becomes a floor).

Climbing is just... hm. You can climb any thing that blocks (including creatures! Why not?); if there's no floor on the z-level above it, you move up a z-level. Actually, this gives me an idea for a weird solution to the 'wall-->floor' problem I mentioned above--all blockable objects generate 'temporary floors' on the z-level above them (assuming the z-level above them doesn't already have a floor, in which case this effect is ignored). This is just a floor type object with some special properties based on what's generating it from below. So you could climb on top of a monster and jump to the floor on the next level (somehow, the monster would be able to resist you, but I don't know how yet).

Or am I thinking too much for this? Is there a simpler solution? I like this one, but again, I have a tendency to like unusual solutions.
Careful, now. Rendering should be handled separate from game logic. You don't want tight coupling there. Ask yourself this: do you want a game module to be able to change the basic rules for how the player interacts with the game? (You might, actually. For instance, there could be a mirror-of-seeing-things-that-are-far-away. There are already good solutions for this sort of extensibility problem, though.)
I was imagining two major rendering calls; one that draws floors (and their contents), and one that draws all the system types on top of it. That would let me do stuff like having a mirror-of-seeing things, since--currently--the way my screen works is by centering on whatever object is held in the variable 'focus' (and the move routine moves the focus around). I could just change 'focus' to a system object, and add a stipulation to the move function that allows it to move regardless of anything (except x-y-z limitations) if it's a system object.
But where do the rules go? Suppose I need to create a type of floor called a "trapdoor". How do I specify the rule that it's only triggered by "blocking" objects?
I'm not actually sure; a property, maybe? That points to an object--an object that somehow contains the ruleset? This is something I assumed I would figure out as I worked, but thinking on it now, it's probably a major architectural concern and if I don't have some thoughts on how to do it now, I'm going to hit some major roadblocks on the way.

I assumed most rulesets would be handled by interacting properties (so a property would be 'on_contact', and somehow point to the 'fall action', applying it to whatever contacts it--and another property would be 'trigger', which when set (to say, 'blocked') would check the triggering object to make sure the trigger was present, and set to True). There are some limitations on that, though, and I'm trying to think through how to overcome them.
Why can't it be both an Effect and a Thing?
FYI, Python does allow one class to have two base classes.
I had imagined 'Type' to be a single property with a single value (I'm enforcing the type system through the property system, rather than creating a whole new object class or another entirely new property of the current object class). I could certainly make 'type' part of the object class, but I'm hesitant about creating new classes if only because I'm worried it'll hamstring the flexibility I'm aiming for. But sub-classes might work, with each type being a sub-class of the main class, and with something like an 'arm' being an object that's inherited the properties of the effect and thing class.
User avatar
The Great Hippo
 
Posts: 5489
Joined: Fri Dec 14, 2007 4:43 am UTC
Location: behind you

Re: (Newbie Warning) Coding Rogue-Clone in Python

Postby The Great Hippo » Sun May 20, 2012 4:04 pm UTC

I just wanted to post here again to mention that I've figured out a few more crucial points while I was working another graveyard shift:

* The solution to the floor problem I was proposing is kind of crappy. There'd have to be special cases all over the place; for instance, a character climbing a wall would need a floor beneath them for each z-level they ascended (assuming they'd yet to reach the top). My solution, then, is to make every x-y-z coordinate on the map have a floor object; I'd therefore rename the floor type to 'cell' to more accurately reflect this. Whether or not a cell actually has a floor is one of its properties.

* I need to investigate tuples and some other data structures to see if it would be an effective method to 'phase out' the x-y-z coordinate system with cells and just use some sort of array or data structure (floor[2][4][1].properties['inventory'] would give me a list of everything inside the floor tile at coordinate 2-4-1, for example). I mean, since I'm going to have a cell on every possible point anyway...

* I've been thinking about your question (how do trap-doors work) a bit more and it lead me to some conclusions about how the Effect type will work. I'm renaming it the Action type, and creating a sub-element inside of it--a list of effects. Each effect consists of list of 'elements'. Each of these 'elements' is a type of value. There are conditional effects (which consist of conditional elements (like 'If True') and elements value (such as 'blocked', which is a key in an object's properties)) and 'acting effects' (which would consist of elements like '1d6' and 'damage' -- the first element would generate a number between 1-6, and the second would apply the value as damage). Every time a round passes--every time an object collides with another--and on several other occasions--we check the container of the object involved for any actions with effects that possess conditional elements that would apply--when they're found, we check the other conditional effects out of that group, and if any of them are true, we add those actions to the 'queue' of actions to be taken (to avoid errors created by effects that destroy or move objects, for example). So a trap door would be a floor with a 'fall' action in its effect; the effects would look like this: Effect 1 (conditional): 'on touch', Effect 2 (acting): 'Caller falls'. An action that would poison you would look like this: Effect 1 (acting): 1d6, damage. Effect 2 (conditional): 'every round'. Effect 3: 1d3, rounds, end effect.

(Actually, that last effect worries me--I'm not sure if it's a conditional or what. I might need a third type of effect, maybe 'duration')

EDIT: Or better yet, store the 'conditional' in Action rather than Effect, and make the conditional in Effect have relevance toward ending the effect (so when an effect says '1d3, round, end', that tells us to destroy the action it's contained in. When an Action says 'every round' (and that's all it would say), it tells us to take this action every round. I need some way to clarify that an action is already being taken and doesn't need to be repeated, though. Maybe the action queue can help with that... Point is, assuming this is a good way to go about it, I think the next major step is to try and generate a simple program that can interpret effects and actions and apply them to objects and their properties.
User avatar
The Great Hippo
 
Posts: 5489
Joined: Fri Dec 14, 2007 4:43 am UTC
Location: behind you

Re: (Newbie Warning) Coding Rogue-Clone in Python

Postby Ben-oni » Sun May 20, 2012 9:13 pm UTC

The Great Hippo wrote:* I need to investigate tuples and some other data structures to see if it would be an effective method to 'phase out' the x-y-z coordinate system with cells and just use some sort of array or data structure (floor[2][4][1].properties['inventory'] would give me a list of everything inside the floor tile at coordinate 2-4-1, for example). I mean, since I'm going to have a cell on every possible point anyway...

There's no need to have an exhaustive array of cells. If the array has no cell at a certain point, the game engine should treat it as an empty cell. So, using arrays to represent the grid is unnecessary. Try "grid[(x,y,z)].properties['inventory']". Either test for "(x,y,z) in grid" before or catch the exception after.

* I've been thinking about your question (how do trap-doors work) a bit more and it lead me to some conclusions about how the Effect type will work. I'm renaming it the Action type, and creating a sub-element inside of it--a list of effects. Each effect consists of list of 'elements'. Each of these 'elements' is a type of value. There are conditional effects (which consist of conditional elements (like 'If True') and elements value (such as 'blocked', which is a key in an object's properties)) and 'acting effects' (which would consist of elements like '1d6' and 'damage' -- the first element would generate a number between 1-6, and the second would apply the value as damage). Every time a round passes--every time an object collides with another--and on several other occasions--we check the container of the object involved for any actions with effects that possess conditional elements that would apply--when they're found, we check the other conditional effects out of that group, and if any of them are true, we add those actions to the 'queue' of actions to be taken (to avoid errors created by effects that destroy or move objects, for example). So a trap door would be a floor with a 'fall' action in its effect; the effects would look like this: Effect 1 (conditional): 'on touch', Effect 2 (acting): 'Caller falls'. An action that would poison you would look like this: Effect 1 (acting): 1d6, damage. Effect 2 (conditional): 'every round'. Effect 3: 1d3, rounds, end effect.

(Actually, that last effect worries me--I'm not sure if it's a conditional or what. I might need a third type of effect, maybe 'duration')

EDIT: Or better yet, store the 'conditional' in Action rather than Effect, and make the conditional in Effect have relevance toward ending the effect (so when an effect says '1d3, round, end', that tells us to destroy the action it's contained in. When an Action says 'every round' (and that's all it would say), it tells us to take this action every round. I need some way to clarify that an action is already being taken and doesn't need to be repeated, though. Maybe the action queue can help with that... Point is, assuming this is a good way to go about it, I think the next major step is to try and generate a simple program that can interpret effects and actions and apply them to objects and their properties.

Event systems have been well-developed over the years. With the advent of a lexically scoped "lambda", perfection was achieved (or as close as makes no difference).

Code: Select all
import random

timerActions = list()
def setTimer(when, what, *args):
   timerActions.append((when, what, args))

# call this each round in the main program loop
def triggerTimerActions():
   global timerActions
   [what(*args) for (when, what, args) in timerActions if when <= 1]
   timerActions = [(when-1, what, args) for (when, what, args) in timerActions if when > 1]

# To roll 1d6+2: dice(1, 6, 2)()
def dice(num, max, mod=0): return lambda: sum([random.randrange(1,max+1) for x in xrange(0,num)]) + mod

def damage(target, hp):
   try:
      target.properties['Hit Points'] -= (hp() if callable(hp) else hp)
      if target.properties['Hit Points'] < 0:
         target.properties['onDeath']({'target': target})
      return True
   except:
      return False

def poison(target, hp, rounds):
   def dealPoison(rounds): # shadowing
      if (round <= 0): return
      damage(target, hp)
      setTimer(1, dealPoison, rounds-1)
   setTimer(1, dealPoison, rounds)
   return True

ancientSword.properties['onHit'] = lambda(event): damage(event['target'], dice(1, 6)) and \
                           poison(event['target'], dice(1, 4), dice(1, 3)())


Note how easy it is to implement timers when you pass functions as parameters. dice is also a neat little tool: instead of giving a random number, it gives a function that gives the random number. That way, you can "pass the dice", so to speak, without having to say which dice they are. Note that if the action depends upon the object, you can't just copy the action. You'll have to create an action generator, like so:
Code: Select all
# A Bloodbound sword deals one additional point of damage for each time it has made a kill
def makeBloodboundSword(sword):
   onHit = sword.properties['onHit']      # The previous onHit action
   sword.properties['bloodbound'] = 0   # The new 'Bloodbound' property
   def bloodboundHit(event):         # The new onHit action
      wasDead = isDead(event['target'])
      onHit(event)
      damage(event['target'], sword.properties['bloodbound'])
      if not wasDead and isDead(event['target']): sword.properties['bloodbound'] += 1
   sword.properties['onHit'] = bloodboundHit


Alternatively you can pass the object receiving the event as part of the event structure.

There is an issue with persistence, but we'll build that bridge when we come to it.
Ben-oni
 
Posts: 268
Joined: Mon Sep 26, 2011 4:56 am UTC

Re: (Newbie Warning) Coding Rogue-Clone in Python

Postby The Great Hippo » Mon May 21, 2012 8:56 am UTC

Ben-oni wrote:There's no need to have an exhaustive array of cells. If the array has no cell at a certain point, the game engine should treat it as an empty cell. So, using arrays to represent the grid is unnecessary. Try "grid[(x,y,z)].properties['inventory']". Either test for "(x,y,z) in grid" before or catch the exception after.
One thing that's confusing me here; grid[(x,y,z)]--the (x,y,z) here is a tuple being used as an index, right? But I thought indexes can only be integers (so grid[0] would bring up the first element of the list 'grid', for example -- while grid[(0, 0, 0)] raises a TypeErrror, noting that indexes must be integers, not tuples).

Would grid[x][y][z] be a better/functional solution? Creating three lists--the bottom (z) list nested in the y list, which is nested in the x list--with some of them assigned a cell object, and others left empty (so we count them as elements, but they aren't detected when we check these lists for a particular cell at a particular coordinate)? Or am I missing something?

(asking, because I'm trying to get the cell structure up and running first, so I can start working with the event structure you described--I totally forgot about anonymous functions (again) and their ability to deal with stuff like that!)

EDIT: Looking at my old, old, old original code, I tackled this problem with the 'exhaustive array of cells' method. I'm wondering if I can use a similar solution, but simply define some some of the elements of this list as 'empty' rather than assigning an object to every one of them. Here's the old code:
Code: Select all
    map = [[[ Object("default", {"movement_blocked" : False,
                                 "explored" : True,
                                 "movement_blocks_sight" : False,
                                 "char" : " ",
                                 "RGB" : [10, 10, 10],
                                 "inventory" : []                  })
            for a in range(0, MAP_Z + 1) ]
                for b in range(0, MAP_WIDTH + 1) ]
                    for c in range(0, MAP_HEIGHT + 1) ]



EDIT-EDIT: Already seeing a problem with this method; you can't change a list element with just a call to its index number--you need to generate a whole new list with the different element. And that's an incredibly sloppy way to add new cells. So the above example would only work for the exhaustive 'cells at every coordinate' method, since all the cell objects I'd ever need would exist at the start, and all I'd be doing would be changing their various properties. So, I'm kind of stumped as to how grid[(x, y, z)] would work; I'm still looking into the possibility of importing the python array method.

(Actually, depending on how difficult adding new cells turns out to be, couldn't the exhaustive cell method turn out to be a more interesting solution? It would make things like tunneling easier (rather than adding a new cell and determining all its properties based on either a default value or the cells around it, you'd just be changing one cell's properties from 'filled' to 'not filled'), and 'open range' maps (like a forest, or just a wide open space) could be accomplished with a few quick keystrokes)
Last edited by The Great Hippo on Mon May 21, 2012 9:32 am UTC, edited 1 time in total.
User avatar
The Great Hippo
 
Posts: 5489
Joined: Fri Dec 14, 2007 4:43 am UTC
Location: behind you

Re: (Newbie Warning) Coding Rogue-Clone in Python

Postby Zamfir » Mon May 21, 2012 9:31 am UTC

You can use tuples as keys in a dictionary. That's in fact an important aspect of them

So 'grid' itself could simply be a dictionary, or it could be an object with a "getCell(x,y,z)" method, that stores cells internally in a dictionary for lookup.

The latter is more complicated, but has the advantage that you could have an orderly "setCell(cell, x, y, z)" method that checks on the sanity of the inputs (like whether the location is already occupied, or whther the cell variable really contains a Cell object). And you can do things like "setBlockOfCells(celltype, xmin, xmax, ymin, ymax, zmin, zmax)" or "setArrayOfCells(listOfCells, listOfCoordinates)"

In a typical Python move, you could even inherit the Grid class from dict. At first you'd do nothing except inherit (so a Grid instance would just be a standard dictionary), then add those extra methods when they come in handy, or override __setitem__ when you discover that you need more checks.

As last thing: do want just one grid? You can have multiple maps with "gates" in between. So if you walk onto the "cave entry" cell you leave this grid and enter another grid. If movement in z-direction is relatively rare, you can even implement different levels as separate grids.
Last edited by Zamfir on Mon May 21, 2012 9:49 am UTC, edited 1 time in total.
User avatar
Zamfir
 
Posts: 5742
Joined: Wed Aug 27, 2008 2:43 pm UTC
Location: Nederland

Re: (Newbie Warning) Coding Rogue-Clone in Python

Postby The Great Hippo » Mon May 21, 2012 9:43 am UTC

Zamfir wrote:You can use tuples as keys in a dictionary. That's in fact an important aspect of them

So 'grid' itself could simply be a dictionary, or it could be an object with a "getCell(x,y,z)" method, that stores cells internally in a dictionary for lookup.

The latter is more complicated, but has the advantage that you could have an orderly "setCell(cell, x, y, z)" method that checks on the sanity of the inputs (like whether the location is already occupied, or whther the cell variable really contains a Cell object). Or do things like "setBlockOfCells(celltype, xmin, xmax, ymin, ymax, zmin, zmax)" or "setArrayOfCells(listOfCells, listOfCoordinates)"

In a typical Python move, you could even inherit the Grid class from dict. At first you'd do nothing except inherit (so a Grid instance would just be a standard dictionary), then add those extra methods when they come in handy, or override __setitem__ when you discover that you need more checks.
That sounds extraordinarily complex, but also extraordinarily interesting. I'm worried that it would make things much more precarious when dealing with ideas like tunneling, or mining, or accidentally teleporting into cells that don't even exist--assuming I want consequences for that action rather than just a 'teleport failed, bwah' message--but a lot of the routines you're describing sound like they could easily address that (try to teleport into a coordinate with no cell? create a cell at that coordinate, deposit you in said cell).

One thing that concerns me about this solution, though--it's something Ben-oni mentioned earlier, about keeping my render methods separate from my game-logic--I imagined some interesting graphical trickses, like carrying a torch and having the light affect walls, and even (very slightly) affect areas outside the boundaries of the walls (to create an illusion of depth--when I'm carrying a torch, it lights up all the squares around me, has a particular affect on the walls around me, and a very subdued effect on the 'unseen' squares around those walls). The way I imagined this working relies very much on an exhaustive cell structure--because my ideas for rendering methods are joined at the hip with the way cells and movement works. This solution might force me to make those functions much more separate. Which might be a good thing, but it's also a thing I have to plan for.

(I think part of my problem is that I first designed this way back with an exhaustive cell method, and a similarly exhaustive render method--one that would go through every square, one by one, and make determinations about what needs to be drawn and what doesn't--that mentality is making me think less in terms like 'define only what you need' and more in terms like 'define everything, then only use what you need')

EDIT: I'm also concerned with how this would impact the transference from the grid object (which would be a type of Grid, a dictionary with modifications) to the map object, which is an object specific to the libtcod console--and represents only a set of x-y cells (no z). It's how the graphic interface works, and is crucial in rendering. So I need a way to filter out all the keys that don't have the right z coordinate and only apply the remaining keys. I suspect this is trivial to accomplish, though.
User avatar
The Great Hippo
 
Posts: 5489
Joined: Fri Dec 14, 2007 4:43 am UTC
Location: behind you

Re: (Newbie Warning) Coding Rogue-Clone in Python

Postby Zamfir » Mon May 21, 2012 10:36 am UTC

The extra methods are just options, you don't have to do them if you don't need them.

The basic idea is just to use a dictionary, which is not complex. It's just a way to say
Code: Select all
myCell = Cell()
myCell.inventory = ['shoes', 'shield']

grid={}
grid[(3,4,5)] = myCell

print( grid[(3,4,5)].inventory[0] ) // 'shoes'


On the issue of nonexistent cells: you have to deal with those anyway, namely at the edge of your map. It's probably more robust and even easier to simply never assume that a cells exists, instead of remembering to handle the (literal) edge cases every time.

Filtering all z=constant cells is not hard and reasonably fast:
Code: Select all
 gridZ = dict(((loc, cell) for loc,cell in grid.items() if loc[2]==currentZ))

But if you do this very often (like many times every second), you will indeed be better off saving the horizontal plane grids separately.
User avatar
Zamfir
 
Posts: 5742
Joined: Wed Aug 27, 2008 2:43 pm UTC
Location: Nederland

Re: (Newbie Warning) Coding Rogue-Clone in Python

Postby The Great Hippo » Mon May 21, 2012 10:55 am UTC

Zamfir wrote:As last thing: do want just one grid? You can have multiple maps with "gates" in between. So if you walk onto the "cave entry" cell you leave this grid and enter another grid. If movement in z-direction is relatively rare, you can even implement different levels as separate grids.
Yeah, I want multiple maps--I was thinking of how to tackle this, but turning grids into dictionaries is probably good enough (grid holds all the entries for the map we're currently on; when you 'portal' to a new map, we overwrite the old grid with the new grid's dictionary). A natural consequence is probably that since game-operations only matter on a one-grid-at-a-time level, when you leave a grid, it's 'frozen', and any changes that occur on it will have to be abstracted. This is probably much more sane than the alternative, though (keep the grid running round-by-round even when you're not on it!).

Checking z-coordinates more than once a second--this is only going to become an issue if I go with real time versus turn-based time. An alternative fix that comes to mind is to keep the book-keeping separate--have 'cube', which is the dictionary of all coordinates (x-y-z), then have 'square', which is the dictionary of all x-y coordinates. Just filter the current z-coordinates into 'square' whenever you change z-levels.

I wonder if this would make determining actions (such as monster movement, for example) on other z-levels tricky, though. Probably not; the importance of filtering it down to one z-level is primarily graphical, and I can imagine game operations mostly dealing with the cube versus the square (square entries just point to cube entries, so anything I change on cube auto-changes for square... I think? I'm pretty sure. Sometimes, it's hard to wrap my brain around how object-oriented programming works).
User avatar
The Great Hippo
 
Posts: 5489
Joined: Fri Dec 14, 2007 4:43 am UTC
Location: behind you

Re: (Newbie Warning) Coding Rogue-Clone in Python

Postby Zamfir » Mon May 21, 2012 11:08 am UTC

The Great Hippo wrote:(square entries just point to cube entries, so anything I change on cube auto-changes for square... I think? I'm pretty sure. Sometimes, it's hard to wrap my brain around how object-oriented programming works).


Yeah, this is the core issue to wrap your head around. A dictionary (or list) contains references to other objects. So the square 'cut' has references to the same objects (let's call them Cells), and if you modify such object, this will be reflected in the cube as well.

The trouble starts if you replace a Cell, or add one, or remove it. If you do this on the cube, the same location in the square will still point to the old Cell.

This becomes important if you use different classes for different kinds of cells. Suppose you have a standard Cell, and a special celltype called Stair (which might inherit from Cell), which behaves different and has extra properties. Then if you swap a Cell for a Stair ont he cube, you get in problems.


EDIT: a matter of terminology: 'overwriting' might not be the best way to think. It contains the implication that you remove things from memory, which is not necessarily what you will do.

imagine this situation:
Code: Select all
allGrids={}
allGrids['inside cave'] = GetMapFromSomewhere()
allGrids['outside cave'] = GetMapFromSomeOtherPlace()

activeGrid = allGrids['inside cave']
activeGrid = allGrids['outside cave']

There's no overwriting here, just pointing to different objects in memory.
User avatar
Zamfir
 
Posts: 5742
Joined: Wed Aug 27, 2008 2:43 pm UTC
Location: Nederland

Re: (Newbie Warning) Coding Rogue-Clone in Python

Postby The Great Hippo » Mon May 21, 2012 11:36 am UTC

My concern is whether or not it's going to be difficult to--when I want to add a cell--actually update the square and grid's pointers appropriately. I guess it's pretty trivial; the same process that gets the square from the grid when I move up or down a z-level can just be applied whenever a cell is created or destroyed on the grid.

Also, I'm working on building a deeper class structure now, using the types. Having different draw functions for different types simplifies a lot of stuff.
User avatar
The Great Hippo
 
Posts: 5489
Joined: Fri Dec 14, 2007 4:43 am UTC
Location: behind you

Re: (Newbie Warning) Coding Rogue-Clone in Python

Postby Zamfir » Mon May 21, 2012 1:37 pm UTC

Mm, to see if I get this right:

[*]the game engine contains a single, 2-d 'map' at a time
[*]this map does not contain all location-specific information you need to run the game, so you inevitably need another datastructure that keeps all information
[*] you want to save location-specific information in cell objects, and each cell object corresponds one-on-one to a grid field in the 'map' of the game engine.
[* you make changes to the information during the game, the cells are not supposed to be read-only
[*]You don't often send a new map to the game engine, right? You would normally send a complete map when you enter a (2-d) area, and make small modifications to the map when needed?
[*] When the player is on a given z-level their actions are regularly influenced by the situation on another z-level (especially above), their actions might result in modifications of other z-levels as well, and time keeps ticking for this other z-level with respect to monster movements etc. This is the problematic part, otherwise you could just work on 1 2-d area at a time.


In that case, I would say that you want two layers to represent the situation. One object (a dictionary, or a list of lists) provides a simple mapping from location to a cell object. Let's call this the grid. A second contains a reference to this grid object, but can also talk to the game engine. let's call this the grid-wrapper.

If you want to make a change to the grid, or to any cell in the grid, you call the relevant function on the grid-wrapper. The gridwrapper then makes the needed modifications to the grid, and checks whther these modification have an impact on the current map in the game engine. If so, it updates the map.

If you move to another 2-d level, you tell this to the grid wrapper. The grid wrapper then finds all the cells on that level, and sends them to the game-engine map.
User avatar
Zamfir
 
Posts: 5742
Joined: Wed Aug 27, 2008 2:43 pm UTC
Location: Nederland

Re: (Newbie Warning) Coding Rogue-Clone in Python

Postby The Great Hippo » Mon May 21, 2012 4:15 pm UTC

I think you're right about basically everything above (but my terminology is really, really shakey). I'm thinking that there's the cube (user-defined dictionary with an x-y-z coordinate system as its key, each x-y-z coordinate corresponding to an instance of the Cell class) and the square (an ordinary dictionary with the same x-y-z coordinate system and pointers, but filtered so only one set of z-coordinates apply--and a function that regularly 'updates' this square based on changes made to the cube whenever there's reason to suspect that the pointers should have changed, such as in the case of z-level change, cell destruction, or cell creation). The square is used for blitting graphics on screen (to reduce how much stuff the game has to sort through), the cube is used for actual important number crunching (where is monster x, what is monster y doing, where is the holy dagger of shut-your-face currently placed).

I finally managed to figure out how to filter a dictionary's contents for the last value of the tuple key (the z-coordinate, in this case). I know you (Zamfir) posted code that would do this just fine, but I didn't quite get it, so I wanted to write some of my own that would accomplish the same thing. It's a few lines longer, but it's my (apparent) first correct use of the lambda function, so I'm happy with it (once I understand your example, I'll probably switch mine out for yours).

Code: Select all
      keys = grid.keys()
      keys = filter(lambda keys: keys[2] == 0, grid.keys())
      for i in keys:
         square[i] = grid[i]
Now, I'm going to do some much-needed maintenance to make the code a little more sensible (renaming 'grid' to 'cube' and probably... uh, keep square I guess? Maybe 'grid' would be better, since grids tend to be two-dimensional), then work on making the contents of floors (their inventory) display properly with the draw function. Once I've got the basics of this interface working, I'll post the code and let the wolves descend upon it to tear it apart.

EDIT: Also I know the proper use of lambda is probably really trivial, but goddamn it took me a while to figure out how, where, and why I should use it (and this is just one instance--lord knows where else I should be using it!). Also, is there a particular naming protocol when it comes to global variables in python? I have a few (the 'cube', the 'grid', the 'focus' (the object which the controls move, and the camera is centered on by default), and I'm thinking it's probably a good idea to differentiate them from other variables. I guess any naming protocol would do, but if there's a standard one, I'd like to stick to it.
User avatar
The Great Hippo
 
Posts: 5489
Joined: Fri Dec 14, 2007 4:43 am UTC
Location: behind you

Re: (Newbie Warning) Coding Rogue-Clone in Python

Postby Zamfir » Mon May 21, 2012 5:51 pm UTC

Yes, I see that the filter code would be weird if you're not used to it. Perhaps a bit too convoluted, but there's useful stuff in there

The underlying trick is a list comprehension. Those are a clean and fast syntax for 'map' and 'filter'.

The basic mapping case is:
Code: Select all
singles = range(2,15)
doubles = [number*2 for number in singles]   
You can also filter, or do both:
Code: Select all
aboveTens = [number for number in singles if number>10]
aboveTenDoubled = [number*2 for number in singles if number>10]


dict.items() returns (key, value) pairs which you can use together:
Code: Select all
names2Numbers = {
    'one': 1,
    'two': 2,
    'three': 3
}
aboveOnes = [name for name, number in names2Numbers.iteritems() if number>1 ]
// ['two', 'three']  or ['three', 'two'], order is uncertain

aboveOnesTuples = [(name, number) for name, number in names2Numbers.items() if number>1]
// [('three', 3), ('two', 2)]

dict(abovesOnesTuples)
//makes a dictionary from the tuples

Now, the last trick is that you don't need to build the whole list, you can make a 'generator', which is an object that produces the contents from the list one after the other and immediately forgets them again. Generators are made the same way, except you use round brackets instead of square brackets.

Which brings us to the magic code
Code: Select all
dict( (name, number) for name, number in names2Numbers.iteritems() if number>1) 

Which neatly filters dictionary into a new dictionary.


Note: Use dict.iteritems() in python 2,dict.items() in python 3.dict.items() in python 2 is bad for large dictionaries because it makes an enitre list
Last edited by Zamfir on Mon May 21, 2012 6:19 pm UTC, edited 1 time in total.
User avatar
Zamfir
 
Posts: 5742
Joined: Wed Aug 27, 2008 2:43 pm UTC
Location: Nederland

Re: (Newbie Warning) Coding Rogue-Clone in Python

Postby The Great Hippo » Mon May 21, 2012 6:09 pm UTC

Also, is there any easy way to derive a key from its corresponding value? I'm trying to retrieve the tuple used to pull up a cell so I know where to draw it, but the only way that seems to work is storing the tuple in the cell's x y z properties, and that seems rather... inelegant (not to mention inefficient; I already have the x-y-z coordinate, I just need to figure out a way to put it into the cell's draw function).

EDIT: Oh--hm. Judging from the code you just posted, would putting this in the draw function under 'Cell' get me the tuple that I need?

Code: Select all
entry = dict((key, value) for key, value in grid.iteritems() if value == self)
xyz = entry.items()
xyz = xyz[0]


I'll give it a try and see what happens.

EDIT-EDIT: Yep, I think that works! With one modification--I need an extra [0] to separate the tuple from the object.
User avatar
The Great Hippo
 
Posts: 5489
Joined: Fri Dec 14, 2007 4:43 am UTC
Location: behind you

Re: (Newbie Warning) Coding Rogue-Clone in Python

Postby Zamfir » Mon May 21, 2012 6:44 pm UTC

A dictionary is a lookup table, or hash table. It is optimized to very quickly find the value that belongs to a specific key (wikipedia will explain how). Every key must be unique, but the same value can be used for multiple keys.

So inverse lookups are not efficient (it's fundamentally just trying keys until the right value pops up), and python subtly discourages you from it.

Why can't you just remember the key, get the cell from the dictionary, and send (key, Cell) to wherever you want?

EDIT: if you understand list comprehensions well, you can just do
Code: Select all
[key for key, value in grid if value==requiredvalue]

That gives you a list of all keys with the requiredvalue as value.

But doing this is a sign that your program is not well-thought out enough.
Last edited by Zamfir on Mon May 21, 2012 6:51 pm UTC, edited 2 times in total.
User avatar
Zamfir
 
Posts: 5742
Joined: Wed Aug 27, 2008 2:43 pm UTC
Location: Nederland

Next

Return to Coding

Who is online

Users browsing this forum: Zabaron and 10 guests