Angband.oook.cz
Angband.oook.cz
AboutVariantsLadderForumCompetitionComicScreenshotsFunniesLinks

Go Back   Angband Forums > Angband > Variants

Reply
 
Thread Tools Display Modes
Old March 11, 2013, 19:53   #1
Magnate
Angband Devteam member
 
Join Date: May 2007
Location: London, UK
Posts: 5,057
Magnate is on a distinguished road
Send a message via MSN to Magnate Send a message via Yahoo to Magnate
Pyrel dev log, part 5

Quote:
Originally Posted by Kinematics View Post
Was doing some thinking regarding the affixType, and why it'd be wanted (it's replacing the hackish handling of tightly coupling the item variant to the item type)
No, I don't think so - at least, that's not why I originally created affixTypes. The idea of affixTypes is to make themed items and affix genera work better. So themes are flavourful (arcane, holy, etc.) rather than code-friendly. I think it could be dangerous to view affixType as the replacement for coupling item to item type.
Quote:
and realized a change to make magic items purely affix-based introduces another problem: how do you associate them with a specific flavor?

The flavors are currently tied to the factory, and thus can always be put on the same item variant each time it's created since there's one factory per variant. However if you get rid of all the ring variants and only have a single ring object (and thus only a single ring factory), you now need to add an extra mapping between the affix chosen and the flavor that goes with it. Not difficult, but something to be aware of.
Yes, this is a fair point. As you say, not impossible, but something to bear in mind during the testing of this approach.
Quote:
Also, allocator rules:

{"index": 261,
"templates": "scroll",
"nameInfo": {"variantName": "Teleportation"},
"allocatorRules": [{"commonness": 40, "minDepth": 10, "maxDepth": -1, "piling": {"chance": 25, "pileSize": "2d1"}}]},

{"index": 420,
"templates": "staff",
"nameInfo": {"variantName": "Teleportation"},
"allocatorRules": [{"commonness": 50, "minDepth": 20, "maxDepth": -1}]},

{"index": 247,
"templates": "amulet",
"nameInfo": {"variantName": "Teleportation"},
"flags": ["TELEPORT", "EASY_KNOW"],
"allocatorRules": [{"commonness": 20, "minDepth": 10, "maxDepth": 40}]},

{"index": 216,
"templates": "ring",
"nameInfo": {"variantName": "Teleportation"},
"mods": [{"bonus": "2", "flags": ["SPEED"]}],
"flags": ["TELEPORT", "EASY_KNOW"],
"allocatorRules": [{"commonness": 20, "minDepth": 5, "maxDepth": 50}]},


For the same affix name, every one of them has different rarities and depth ranges, plus, scrolls need a piling value. Most of it is easy enough, since the same fields are available in item allocators and affix allocators, but you'd have to add piling rules into affix allocator rules, which seems a little odd, though necessary if you divorce the item allocation from the variants.
You're right that that is odd - and worth thinking about. But the different rarities and depths are already catered for in the affix handling: for the same affix you can specify different allocator rules for different item categories.
Quote:
The other issue is the mods and flags per item. I'm not sure those are easily translatable to the affix environment, since the affix is typically just a single effect that you can specify different rules for when it shows up on different items. The above, however, are two that are effects, and two that are flags, and one that has a secondary mod attached.
Affixes provide one thing each, whether that's a mod (pval), a flag (binary) or an effect (activation). So the ring and amulet have the binary flag for uncontrolled random teleportation (one affix), and the scroll and staff have the activation for teleportation (a different affix). To be done neatly, these affixes would need different name (I'd go for "of Random Teleportation" for the first).

The ring would need two affixes to be generated - the random teleportation affix and the speed affix. This is already achievable via themes - if a ring gets teleportation, it automatically gets +2 speed. Theme names are independent of affix names, so that could solve the naming problem too (if we let teleportation amulets also get +2 speed ...).

Thanks for the thoughts - keep them coming.
__________________
"3.4 is much better than 3.1, 3.2 or 3.3. It still is easier than 3.0.9, but it is more convenient to play without being ridiculously easy, so it is my new favorite of the versions." - Timo Pietila
Magnate is offline   Reply With Quote
Old March 11, 2013, 20:04   #2
Derakon
Prophet
 
Derakon's Avatar
 
Join Date: Dec 2009
Posts: 9,024
Derakon is on a distinguished road
Thinking about this whole affix-drives-the-name thing, I really think we are overcomplicating magical item creation. Just because rings and amulets and potions and scrolls and so on could be handled by the affix system doesn't mean that this is the right way to do it. In particular, adding a new magical item becomes more complicated under such a scheme:

* You need to understand not just the object data files but also the affix files.
* Per what you just said, Magnate, you would likely also need to understand the theme system.
* Adding any new affix makes all of the other affixes marginally less common (conflation of base object and affix allocation rates).

All of this so, what, we can avoid having a second naming function? Is it really worth it? What would be so bad about having an alternate naming function for flavored items that just says "[Flavor] BaseType of SubType"?

I think we've gotten a bit too embedded in the problem; step back a bit, and consider whether the solution being proposed is really the right step forward.

Also, Magnate, use a better reader.
Derakon is offline   Reply With Quote
Old March 11, 2013, 20:44   #3
Nick
Vanilla maintainer
 
Nick's Avatar
 
Join Date: Apr 2007
Location: Canberra, Australia
Age: 55
Posts: 8,460
Donated: $60
Nick will become famous soon enough
Quote:
Originally Posted by Derakon View Post
Also, Magnate, use a better reader.
The internet reached perfection twenty years ago, and it's been downhill since then
__________________
One for the Dark Lord on his dark throne
In the Land of Mordor where the Shadows lie.
Nick is offline   Reply With Quote
Old March 11, 2013, 20:57   #4
Kinematics
Apprentice
 
Join Date: Feb 2013
Posts: 71
Kinematics is on a distinguished road
Quote:
Originally Posted by Magnate
Quote:
Originally Posted by Kinematics
Was doing some thinking regarding the affixType, and why it'd be wanted (it's replacing the hackish handling of tightly coupling the item variant to the item type)
No, I don't think so - at least, that's not why I originally created affixTypes.
It's not why affixTypes were originally created, but it's a significant element of how the naming code is using them. And I was noting that variant names are the 'hackish' implementation, and that switching solely to affixes would be a cleaner approach. However, for that to work, the affixType has to cooperate.

I agree with Derakon, though, that this seems to be heading in the direction of making item definition far more complicated.


Another example issue with the difference between the affix and the item:
Code:
{"index": 190,
"templates": "ring",
"nameInfo": {"variantName": "Strength"},
"mods": [{"bonus": "1+M5", "flags": ["STR"]}],
"flags": ["SUST_STR", ""],
"allocatorRules": [{"commonness": 50, "minDepth": 30, "maxDepth": -1}]},
This would allow a ring with a strength bonus between +1 and +6.

Code:
  {
    "index": 183, 
    "mods": [
      {
        "bonus": "d2", 
        "minimum": "1", 
        "flags": [
          "STR"
        ]
      }
    ], 
    "allocatorRules": [..snip..], 
    "name": "Strength",
    "affixType": "physical",
    "genus": "the Body"
  },
The affix gives a bonus between 1 and 2.

Unless you want to create yet another name for a slight difference in the value generation, you'd need to modify the affix structure so that its properties could be specified per item type it got applied to (eg: 1d2 for armor, 1+M5 for rings).

Last edited by Kinematics; March 11, 2013 at 21:32.
Kinematics is offline   Reply With Quote
Old March 11, 2013, 21:32   #5
Kinematics
Apprentice
 
Join Date: Feb 2013
Posts: 71
Kinematics is on a distinguished road
Quote:
Originally Posted by Derakon
While there's certainly the potential for making them more complicated, we need to make certain that simple procs only need simple definitions and simple bits of code to work.
Simple is good, but we don't want it to be so simple that it's awkward to get even mildly complex tasks done. When you want to do less, it shouldn't get in the way, but when you want to do more, it should be easy to grow. Procs seem to be intended to take on a huge portion of the workload for game behavior, so I'm trying to see what sorts of behaviors are easy or difficult to apply.

Quote:
Regarding your specific example of handling the weight of the player's pack via procs, while you could do that I don't think it's a very good idea. There's so many potential ways for an item to no longer be in the pack (dropped, equipped, consumed, destroyed, stolen, etc.), and every single one would need to invoke the correct proc trigger. That's an awful lot of triggers for a single proc definition. Moreover, every single item in the game is going to have the same result for them weight-wise (i.e. add/remove weight to/from the player), so we don't really gain anything by "proc-izing" the behavior. This is one bit of game logic that I'm happy to let the engine handle. Procs could, however, handle what to do when the player is overweight.
Well, it wasn't so much written as "how should it be done?", as "what sorts of triggers could possibly be needed and how/when would they be used?". As you say, there's lots of different ways for an item to be affected; I was trying to get a better grasp of what all those might be, primarily to see what implications it would have for how the procs need to work.

Quote:
Finally, I'm not averse to allowing a Proc definition to have multiple trigger conditions, but recognize that the trigger condition also determines the parameters that the trigger() function receives. E.g. an "item use" trigger provides the item, the user, and the GameMap, while a "creature death" trigger would provide the creature, the killer, and the GameMap. Probably there would be many trigger conditions that pass the same parameter sets, but there'd be nothing stopping you from creating a Proc whose trigger() function would crash when triggered the "wrong way". I think we'd need to resolve this issue before we can consider multiple-trigger Procs to be a finished feature.
Good point, on the different parameter lists. I need to give this some more thought.
Kinematics is offline   Reply With Quote
Old March 11, 2013, 21:52   #6
Derakon
Prophet
 
Derakon's Avatar
 
Join Date: Dec 2009
Posts: 9,024
Derakon is on a distinguished road
Quote:
Originally Posted by Kinematics View Post
Simple is good, but we don't want it to be so simple that it's awkward to get even mildly complex tasks done. When you want to do less, it shouldn't get in the way, but when you want to do more, it should be easy to grow. Procs seem to be intended to take on a huge portion of the workload for game behavior, so I'm trying to see what sorts of behaviors are easy or difficult to apply.
This is certainly true. At the same time I don't think we should expect to do all game logic via Procs. I'd say that if what you want to do can't be reasonably encapsulated by a single Proc, then either you should move that behavior into the engine, or you should add a new trigger to enable the Proc properly.
Derakon is offline   Reply With Quote
Old March 11, 2013, 22:44   #7
Kinematics
Apprentice
 
Join Date: Feb 2013
Posts: 71
Kinematics is on a distinguished road
It would be easy to have a function in the base Proc class that wraps the distinction between single and multiple triggers, so the external code wouldn't need to know about that. However with different parameters you'd have to also do some kind of branching function, like:

Code:
class myProc(proc.Proc):
    def trigger(self, trigger, *args):
        if trigger == "condition 1":
            self.trigger1(args)
        elif trigger == "condition 2":
            self.trigger2(args)
            
    def trigger1(self, *args):
        ~stuff

    def trigger2(self, *args):
        ~stuff
And to make it generically consistent you'd have to call every proc with the trigger that triggered it as the first parameter, which many/most procs wouldn't care about.

You could get around some of that with an internal mapping dict, but that makes it more difficult to define simple stuff. Unless....

[Note: this may not actually work; writing down my thought process as I work through details]

Add in a util curry class (since Python doesn't have it natively), then you could add something like this to the base class (subclasses don't have to touch it at all, except maybe triggerMaps):

Code:
class proc():
    init
        self.triggerMaps = {}
        
    def getTriggerFunction(triggerCondition):
        # Validate argument
        if not isinstance(triggerCondition, basestr):
            return None
        # If only provided a single string as a trigger condition
        if isinstance(self.triggerConditions, basestr):
            if triggerCondition == self.triggerConditions:
                return self.trigger
        # If provided a list of strings as trigger conditions,
        # create a curried version of the function to return.
        elif isinstance(self.triggerConditions, list):
            if triggerCondition in self.triggerConditions:
                return curry(self.trigger, triggerCondition)
        # If provided a dict of trigger conditions, return the
        # function defined for the provided triggerCondition
        # (function value kept in class-defined triggerMaps).
        elif isinstance(self.triggerConditions, dict):
            if triggerCondition in self.triggerConditions:
                return self.triggerMaps[triggerConditions[triggerCondition]]
        # If none of the above are true, return None.
        return None
Then you could define a proc with any of

Code:
          {"name": "remove weight", "triggerCondition": "onDrop"},
          {"name": "remove weight", "triggerCondition": ["onDrop", "onWaggle"]},
          {"name": "remove weight", "triggerCondition": {"onDrop": "wiggle"}},
And it would be called from the item class like so:

Code:
    ## Trigger procs that have the specified trigger.
    def triggerProcs(self, user, gameMap, trigger, *args, **kwargs):
        triggerProcs = [p.getFunction(trigger) for p in self.procs]
        for proc in triggerProcs:
            if proc:
                proc(self, user, gameMap, *args, **kwargs)
Note: doesn't include the pre-action/post-action separation here.

The call works the same regardless of the proc trigger condition type.

Unfortunately, while the -call- works, the code execution point has problems. To allow for lists of trigger actions, and assuming we want to allow the proc to know what triggered it, you'd need both

def trigger(self, *args):
and
def trigger(self, triggerCondition, *args):

defined, since you don't know if/when a list will ever be used on the calling side. I think we can bypass this, though.

First: If triggerMaps is empty, then we don't care what trigger condition called us; we'll always use the standard trigger() function. As such, the trigger condition value is irrelevant.
Second: If triggerMaps has values, we can use that to map the list value to a specific function, while not needing to know explicitly what value was passed in. So again, we can get away without needing to define a triggerCondition parameter on trigger().

Code:
class proc():
    init
        self.triggerMaps = {}
        
    def getTriggerFunction(triggerCondition):
        # Validate argument
        if not isinstance(triggerCondition, basestr):
            return None
        # If only provided a single string as a trigger condition
        if isinstance(self.triggerConditions, basestr):
            if triggerCondition == self.triggerConditions:
                return self.trigger
        # If provided a list of strings as possible trigger conditions,
        # use the function pointed to via triggerMaps.
        elif isinstance(self.triggerConditions, list):
            if triggerCondition in self.triggerConditions:
                if self.triggerMaps:
                    if triggerCondition in self.triggerMaps:
                        return self.triggerMaps[triggerCondition]
                    elif "*" in self.triggerMaps:
                        return self.triggerMaps["*"]
                    else:
                        return self.trigger
                else:
                    return self.trigger
        # If provided a dict of trigger conditions, return the
        # function defined for the provided triggerCondition
        # (function value kept in class-defined triggerMaps).
        elif isinstance(self.triggerConditions, dict):
            if triggerCondition in self.triggerConditions:
                mappedTrigger = triggerConditions[triggerCondition]
                if self.triggerMaps:
                    if mappedTrigger in self.triggerMaps:
                        return self.triggerMaps[mappedTrigger]
                    elif "*" is self.triggerMaps:
                        return self.triggerMaps["*"]
                    else:
                        return self.trigger
                else:
                    return self.trigger
        # If none of the above are true, return None.
        return None

Now in the object (or whatever) definition, you can provide a specific trigger condition, a list of trigger conditions, or a mapping of trigger conditions, and any one would work.

If the proc only defines a trigger() function, it can be called if the proc was defined with either a simple string for a trigger condition, or a list. If it's a list, then any item in the list will call trigger(), and no distinction will be made based on the actual trigger condition.

If the proc defines a triggerMap, then creating the proc with either a list or a dict will cause it to try to look up the value in the map to determine which function to actually call for each available trigger condition (the dict maps the triggerCondition argument to another value first). If it's listed explicitly, it calls the specified function; if the triggerMap includes a "*" (general glob indicator), it will call that for any condition not otherwise specified. Otherwise it will simply call the default trigger() function.


That would allow any degree of simplicity or complexity on the calling side of things. What about different numbers of parameters, though?

Well, any given called proc has to, by definition, handle the expected parameters for a given event. If a "get name" event is going to call all procs with arguments of an item and a list of callbacks, then every proc that is intended to be used for a "get name" event must have a function signature that can handle that.

The the "get name" proc was altered so that it could handle "get item name" and "get creature name" events, and (for the sake of example) if if the creature event passed in the creature, its position, and a list of callbacks, then you'd need to handle those differences.

The triggerMap could map it as: {"get item name": self.getItemName, "get creature name": self.getCreatureName}. Then when a "get creature name" event happened, it would return a function that had a signature that could return appropriate values.

Then you could do something like add a petrification curse that created a proc of {"name": "get name", "triggerCondition": {"get creature name": "get item name"}}, mapping a request for a creature's name to the "get item name" function instead, making it look like a real item. (Could maybe do similarly with mimics.)

That of course leads to the question of overriding a proc instead of just supplementing it, since by default such an additional "get name" function would be called in addition to the existing one, leading to general brokenness.


So we still need to handled triggering vs trigger, and how to override (temporarily, without removing) an existing proc. Will get to that in the next post.

Last edited by Kinematics; March 11, 2013 at 22:54.
Kinematics is offline   Reply With Quote
Old March 11, 2013, 22:53   #8
Kinematics
Apprentice
 
Join Date: Feb 2013
Posts: 71
Kinematics is on a distinguished road
Quick revision: the triggerMaps code can be simplified, since it's really just a repetition of the same logic.

Code:
class proc():
    init
        self.triggerMaps = {}
        
    def getTriggerFunction(triggerCondition):
        # Validate argument
        if not isinstance(triggerCondition, basestr):
            return None
            
        useTrigger = None
        
        # If initialized with a simple string, that's all we
        # need to check.
        if isinstance(self.triggerConditions, basestr):
            if triggerCondition == self.triggerConditions:
                useTrigger = triggerCondition
        # If initialized with a list of strings, check if the
        # triggerCondition is in that list.
        elif isinstance(self.triggerConditions, list):
            if triggerCondition in self.triggerConditions:
                useTrigger = triggerCondition
        # If initialized with a dict of trigger conditions, check
        # if the triggerCondition is in the keys; if so, use
        # the value mapping.
        elif isinstance(self.triggerConditions, dict):
            if triggerCondition in self.triggerConditions:
                useTrigger = triggerConditions[triggerCondition]
                
        # Whatever we ended up with as the trigger, check to
        # see if it's present in triggerMaps; if so, use the
        # specified function.  Otherwise just use the default.
        if useTrigger:
            if useTrigger in self.triggerMaps:
                return self.triggerMaps[useTrigger]
            else:
                return self.trigger
        else:
            # If none of the above are true, return None.
            return None

Last edited by Kinematics; March 12, 2013 at 00:21. Reason: name glob is redundant
Kinematics is offline   Reply With Quote
Old March 12, 2013, 00:14   #9
Magnate
Angband Devteam member
 
Join Date: May 2007
Location: London, UK
Posts: 5,057
Magnate is on a distinguished road
Send a message via MSN to Magnate Send a message via Yahoo to Magnate
Quote:
Originally Posted by Derakon View Post
Thinking about this whole affix-drives-the-name thing, I really think we are overcomplicating magical item creation. Just because rings and amulets and potions and scrolls and so on could be handled by the affix system doesn't mean that this is the right way to do it. In particular, adding a new magical item becomes more complicated under such a scheme:

* You need to understand not just the object data files but also the affix files.
* Per what you just said, Magnate, you would likely also need to understand the theme system.
* Adding any new affix makes all of the other affixes marginally less common (conflation of base object and affix allocation rates).

All of this so, what, we can avoid having a second naming function?
No. I have wanted to move rings and amulets to affix-based generation for as long as I've thought about affixes. This is just a handy incentive to get started. It is true that this doesn't necessarily mean that consumables also need to move to the same system, but I can see advantages beyond the naming issue.

I don't think understanding affixes and themes will be that complicated in the end. I reckon I'll be able to write a simple guide to adding a new item in a paragraph. Better still, I'll be able to write a guide to adding a whole new TYPE of item in two paragraphs - something which would take pages in V and quite a bit in Pyrel as it stands.

Adding a new affix only makes "marginally less common" those affixes which are legal for the same item at the same depth, so I don't think it's a huge issue.
__________________
"3.4 is much better than 3.1, 3.2 or 3.3. It still is easier than 3.0.9, but it is more convenient to play without being ridiculously easy, so it is my new favorite of the versions." - Timo Pietila
Magnate is offline   Reply With Quote
Old March 12, 2013, 00:27   #10
Kinematics
Apprentice
 
Join Date: Feb 2013
Posts: 71
Kinematics is on a distinguished road
pre-event/post-event handling:

I think the triggerMap setup allows fairly simple access to pre-event handling.

triggerMap = {"prepare to cast": self.prepareToCast, "cast": self.trigger}

Then you can add proc definitions

item 1: {"name": "casting", "triggerCondition": "cast"},
item 2: {"name": "casting", "triggerCondition": ["prepare to cast", "cast"]},
item 3: {"name": "casting", "triggerCondition": ["boom"]},
item 4: {"name": "casting", "triggerCondition": {"preboom": "prepare to cast", "boom": "cast"}},

Any proc can then (if applicable) encapsulate logic for pre-event stuff without any single proc instance needing to use it, and without needing no-op functions, simply by including that event type in its trigger map.

Aside: note that even though (or because) "boom" in item 3 isn't in the trigger map, it will still get mapped to the proc's trigger() function. In item 4, it explicitly maps some other events to the proc's functions.

Last edited by Kinematics; March 12, 2013 at 00:42.
Kinematics is offline   Reply With Quote
Reply


Currently Active Users Viewing This Thread: 1 (0 members and 1 guests)
 
Thread Tools
Display Modes

Posting Rules
You may not post new threads
You may not post replies
You may not post attachments
You may not edit your posts

BB code is On
Smilies are On
[IMG] code is On
HTML code is Off

Forum Jump

Similar Threads
Thread Thread Starter Forum Replies Last Post
Pyrel dev log, part 4 Magnate Variants 116 March 11, 2013 18:53
Pyrel dev log, part 3 Derakon Variants 108 November 12, 2012 22:30
Affixes in Pyrel Magnate Development 0 October 13, 2012 12:53
Pyrel dev log, part 2 Derakon Variants 126 September 11, 2012 22:03
Pyrel dev log Derakon Variants 64 June 8, 2012 10:58


All times are GMT +1. The time now is 10:40.


Powered by vBulletin® Version 3.8.11
Copyright ©2000 - 2020, vBulletin Solutions Inc.