Short summary: below, I suggest a possible object model for Angband, and invite you to poke holes in it.
Every once in awhile someone will talk about how they want to rewrite Angband in some more recent language, usually Python. This isn't particularly surprising -- the Vanilla Angband codebase is around 240k lines of C, and isn't always particularly easy to understand, so the impulse to just chuck twenty years' worth of development effort and start fresh is always tempting. Especially since Angband does a lot of things "by hand" that you can delegate to libraries now -- event and window management, file handling, resource marshalling, display logic, even random number generators. Not to mention that interpreted languages tend to be much more pleasant to make cross-platform -- no more main-* files!
So there's a lot of reasons to like the idea of rewriting Angband with a more modern approach to software development. And people have tried it, too -- Sirridan was working on a Python implementation awhile back, though it appears to have stalled. Part of the problem is that Angband is so massive that just using a "better" language isn't enough on its own -- you need to have a good design too. There's a lot of detail to handle!
So just for fun, and as a design exercise, and because I only got four hours of sleep last night so hey why not, here's a somewhat vague object-model design for Angband.
We'll start with the map. The map is of course a 2D grid of containers, we'll call them Cells. Each Cell can hold an arbitrary number of Things, which are basically any "real world" thing you can interact with -- monsters, items, terrain, etc.
Since Things often need to be able to find each other in order to interact with them, the Map will have a collection of Thing Sets, which Things can register themselves with. For example, a new monster registers itself with the monster list, which can later be used when casting detection spells, targeting effects, or listing visible monsters. Thing Sets are created and destroyed on an as-needed basis; if a nonexistent list is requested then of course the result is empty. Among other things, this lets us create associations -- for example, when a group of jackals is created, they request a new Thing Set so they can associate with each other, which opens new possibilities for pack AI.
I use the term "Set" here -- if you aren't familiar with the term, sets are collections of objects where each object can be in the set only once. They have the useful property of O(1) (i.e. fast) lookup times to see if an object is in the set. You can also perform set operations on them, like union, difference, and (especially useful for us) intersection. For example, if you want to find all monsters that are evil, then you could either get the "Monster" Thing Set and then iterate over all the entries, or you could have a second Thing Set of evil Things and intersect it with the monster set.
The bulk of the game is in interactions between Things -- a collection of verbs to move Things, hurt Things, equip Things, disarm Things, etc. For now we assume that every verb is transitive (that is, it is performed by one Thing onto one or more other Things); intransitive verbs can simply have the object of the verb be the subject. Of course not all verbs can apply to all targets. We implement verbs as paired functions: one on the Thing performing the verb (the subject), and one on the Thing that is being verbed, so to speak (the object). These verbs have defined interfaces, and may have implications for the Things implementing them. For example, any valid objects of the "attack" verb must have hitpoints.
On creation, Things register with appropriate Thing Sets for all the verbs they are valid objects for. Thus, when, for example, a trap is created, it registers with the "Disarm" Thing Set, which states that it is a valid object for the "disarm" verb. When another Thing decides to perform that verb, they can go to the Map and say "What Things are available for disarming?", and the Map looks up the appropriate Set and returns them.
We can combine this with targeting filters to narrow the set of possible target Things down even further. For example, the standard Disarm action is only valid for Things adjacent to the actor. So we request from the Map a synthetic Thing Set of all Things adjacent to that actor, intersect it with the Disarmable Things, and voila -- our possible targets for disarming! Alternately, you cast the Disarming spell, which removes all traps along a line -- so you ask the Map for all Things along that line, and do the intersection that way.
This all sounds very complicated, but in implementation it should be straightforward. it has several useful features:
1) It doesn't differentiate between the player and any random monster. The only difference between the player and monsters is that the player chooses verbs based on input, while monsters choose verbs based on AI. Practically speaking, anything the player can do, a monster could potentially do.
2) Inheritance and composition make it easy to have standard responses to various verbs while still providing room for special behaviors. Trapped doors, or even equippable monsters, would be comparatively straightforward and unhackish to implement.
3) We can stuff as many things as we like into each Cell in the Map and only have to worry about them when they're actually relevant. We don't have to create a separate layer for each possible category of terrain.
Some more examples:
* The player wants to cast Detect Monsters. He queries the Map for the Monster Thing Set, filtered by the radius of effect of the spell. The spell sets the "visible" flag on the monsters in the set for one round.
* The player wants to fire an arrow at the closest monster. This requires multiple objects -- the arrow to be shot, (implicitly the bow being used), and the monster to be targeted. We assume that the player, like many other Things, has an inventory and equipment composited onto it; like the Map, these can be queried for Things that are valid targets of verbs. So we query both to get a list of valid ammo, which we present to the user to select from; then we query the Map for a list of Shootable Things in LOS of the player's location. When the arrow is fired, its quantity in the player's inventory is decremented and a new Thing is created that is a quantity-1 copy of that arrow; meanwhile, damage rolls and the like are performed on the monster.
* A monster wants to move towards the player. He requests the Player Set from the Map, gets the first (only) entry from it, and runs a pathfinding routine to generate a path from itself to the player. This routine checks squares to see if the monster can move through them via the Move verb. Every Thing that implements the Move verb has to be able to decide if it can be moved through by other Things. Some Things might be "opaque" to movement (e.g. permanent walls), some might be qualitatively opaque (normal walls, depending on if the monster is a wallwalker; monsters, depending on if the monster can trample them), and of course some are open. Based on this path the monster can then execute the Move verb into the best available Cell.
I've omitted a bunch of details -- how do we structure the monster class? How do we handle equipment bonuses? How is level generation handled? How do we plan to prompt the user for actions and selections? And so on. I consider those to be less fundamental, ergo less important. What we have here is the basic object model for the game at a very abstract level; practically everything else is filling in blanks.
Every once in awhile someone will talk about how they want to rewrite Angband in some more recent language, usually Python. This isn't particularly surprising -- the Vanilla Angband codebase is around 240k lines of C, and isn't always particularly easy to understand, so the impulse to just chuck twenty years' worth of development effort and start fresh is always tempting. Especially since Angband does a lot of things "by hand" that you can delegate to libraries now -- event and window management, file handling, resource marshalling, display logic, even random number generators. Not to mention that interpreted languages tend to be much more pleasant to make cross-platform -- no more main-* files!
So there's a lot of reasons to like the idea of rewriting Angband with a more modern approach to software development. And people have tried it, too -- Sirridan was working on a Python implementation awhile back, though it appears to have stalled. Part of the problem is that Angband is so massive that just using a "better" language isn't enough on its own -- you need to have a good design too. There's a lot of detail to handle!
So just for fun, and as a design exercise, and because I only got four hours of sleep last night so hey why not, here's a somewhat vague object-model design for Angband.
We'll start with the map. The map is of course a 2D grid of containers, we'll call them Cells. Each Cell can hold an arbitrary number of Things, which are basically any "real world" thing you can interact with -- monsters, items, terrain, etc.
Since Things often need to be able to find each other in order to interact with them, the Map will have a collection of Thing Sets, which Things can register themselves with. For example, a new monster registers itself with the monster list, which can later be used when casting detection spells, targeting effects, or listing visible monsters. Thing Sets are created and destroyed on an as-needed basis; if a nonexistent list is requested then of course the result is empty. Among other things, this lets us create associations -- for example, when a group of jackals is created, they request a new Thing Set so they can associate with each other, which opens new possibilities for pack AI.
I use the term "Set" here -- if you aren't familiar with the term, sets are collections of objects where each object can be in the set only once. They have the useful property of O(1) (i.e. fast) lookup times to see if an object is in the set. You can also perform set operations on them, like union, difference, and (especially useful for us) intersection. For example, if you want to find all monsters that are evil, then you could either get the "Monster" Thing Set and then iterate over all the entries, or you could have a second Thing Set of evil Things and intersect it with the monster set.
The bulk of the game is in interactions between Things -- a collection of verbs to move Things, hurt Things, equip Things, disarm Things, etc. For now we assume that every verb is transitive (that is, it is performed by one Thing onto one or more other Things); intransitive verbs can simply have the object of the verb be the subject. Of course not all verbs can apply to all targets. We implement verbs as paired functions: one on the Thing performing the verb (the subject), and one on the Thing that is being verbed, so to speak (the object). These verbs have defined interfaces, and may have implications for the Things implementing them. For example, any valid objects of the "attack" verb must have hitpoints.
On creation, Things register with appropriate Thing Sets for all the verbs they are valid objects for. Thus, when, for example, a trap is created, it registers with the "Disarm" Thing Set, which states that it is a valid object for the "disarm" verb. When another Thing decides to perform that verb, they can go to the Map and say "What Things are available for disarming?", and the Map looks up the appropriate Set and returns them.
We can combine this with targeting filters to narrow the set of possible target Things down even further. For example, the standard Disarm action is only valid for Things adjacent to the actor. So we request from the Map a synthetic Thing Set of all Things adjacent to that actor, intersect it with the Disarmable Things, and voila -- our possible targets for disarming! Alternately, you cast the Disarming spell, which removes all traps along a line -- so you ask the Map for all Things along that line, and do the intersection that way.
This all sounds very complicated, but in implementation it should be straightforward. it has several useful features:
1) It doesn't differentiate between the player and any random monster. The only difference between the player and monsters is that the player chooses verbs based on input, while monsters choose verbs based on AI. Practically speaking, anything the player can do, a monster could potentially do.
2) Inheritance and composition make it easy to have standard responses to various verbs while still providing room for special behaviors. Trapped doors, or even equippable monsters, would be comparatively straightforward and unhackish to implement.
3) We can stuff as many things as we like into each Cell in the Map and only have to worry about them when they're actually relevant. We don't have to create a separate layer for each possible category of terrain.
Some more examples:
* The player wants to cast Detect Monsters. He queries the Map for the Monster Thing Set, filtered by the radius of effect of the spell. The spell sets the "visible" flag on the monsters in the set for one round.
* The player wants to fire an arrow at the closest monster. This requires multiple objects -- the arrow to be shot, (implicitly the bow being used), and the monster to be targeted. We assume that the player, like many other Things, has an inventory and equipment composited onto it; like the Map, these can be queried for Things that are valid targets of verbs. So we query both to get a list of valid ammo, which we present to the user to select from; then we query the Map for a list of Shootable Things in LOS of the player's location. When the arrow is fired, its quantity in the player's inventory is decremented and a new Thing is created that is a quantity-1 copy of that arrow; meanwhile, damage rolls and the like are performed on the monster.
* A monster wants to move towards the player. He requests the Player Set from the Map, gets the first (only) entry from it, and runs a pathfinding routine to generate a path from itself to the player. This routine checks squares to see if the monster can move through them via the Move verb. Every Thing that implements the Move verb has to be able to decide if it can be moved through by other Things. Some Things might be "opaque" to movement (e.g. permanent walls), some might be qualitatively opaque (normal walls, depending on if the monster is a wallwalker; monsters, depending on if the monster can trample them), and of course some are open. Based on this path the monster can then execute the Move verb into the best available Cell.
I've omitted a bunch of details -- how do we structure the monster class? How do we handle equipment bonuses? How is level generation handled? How do we plan to prompt the user for actions and selections? And so on. I consider those to be less fundamental, ergo less important. What we have here is the basic object model for the game at a very abstract level; practically everything else is filling in blanks.
Comment