1

Say I was creating a 'Character' model in Django for an RPG, and I wanted all the attributes (Strength, Dexterity, Stamina etc.) to be defined in a similar way, and be able to be treated in a similar way.

Obviously I could do this:

intelligence = IntegerRangeField(min_value=1, max_value=10)
wits= IntegerRangeField(min_value=1, max_value=10)
resolve = IntegerRangeField(min_value=1, max_value=10)
strength = IntegerRangeField(min_value=1, max_value=10)
dexterity = IntegerRangeField(min_value=1, max_value=10)
stamina = IntegerRangeField(min_value=1, max_value=10)
presence = IntegerRangeField(min_value=1, max_value=10)
manipulation = IntegerRangeField(min_value=1, max_value=10)
composure = IntegerRangeField(min_value=1, max_value=10)

IntegerRangeField definition

But that's not particularly DRY. It also means when I get to add ~30 skills that are similar I'm going to be even less dry, and prone to mistakes.

These don't seem worth creating via foreign keys (which is the solution here), as I'd need to specify 9 of them for each attribute etc.

Is there a solution to this already that is DRY? Or will I need to roll my own?

Edit:
My Primary use of attributes and skills is to provide totals for skill checks, for instance if I want to know how many dice to roll for Programming I might specify it uses the characters intelligence+computers attributes and skill, and for example from a list of 'Tasks' (e.g. other combinations of attributes and skills) show the top five that character can perform.


Edit:

Looks like following the M2M route has lead me to overnormalization. Would anyone be able to suggest improvements?

Community
  • 1
  • 1
AncientSwordRage
  • 7,086
  • 19
  • 90
  • 173
  • 1
    Could the downvoter let me know how I could improve my question? I know this is a fine line I'm treading but I want to be on the right side of it. – AncientSwordRage Jan 27 '15 at 12:00
  • 1
    with you edit; i would think that a manytomany relationship to a 'skills' model would be your best bet; that way you can quickly grab a 'Characters' specific 'skills' entry and apply it to your `intelligence+computers', etc, all with database queries. – warath-coder Jan 27 '15 at 12:53
  • need to make that document public – warath-coder Jan 28 '15 at 12:34
  • @warath-coder https://docs.google.com/spreadsheets/d/1DM3xTN_nRBuAcpgj-2K3LiC7dzQNxIjctIiSJQ3KOlA/edit?usp=sharing how is that? – AncientSwordRage Jan 28 '15 at 19:10

4 Answers4

4

You can use many to many field relation for this kind of situation (like @RonaldOldenburger suggested) where you need to repeat your fields. For example:

class Skill(models.Model):
    name = models.CharField(max_length=255)
    value = IntegerRangeField(min_value=1, max_value=10)

class Character(models.Model):
    name = models.CharField(max_length=255)
    skill = models.ManyToManyField(Skill)

And if you use modelform to create a Character model instance, then you can use model formset factory to add Skillto it.

ruddra
  • 50,746
  • 7
  • 78
  • 101
  • Could you provide more details. Which model do you mean by 'given model'? I understand how proxy models work, but not how you think they apply. – AncientSwordRage Jan 27 '15 at 12:10
  • Sorry, my bad, I have misunderstood the question, updated it accordingly – ruddra Jan 27 '15 at 13:48
  • 1
    this is exactly what i meant in my comments, but was too lazy to write out :) – warath-coder Jan 28 '15 at 12:33
  • This is the answer I went with. I can't judge if it's the best answer, but it works. I also implemented a custom through table. – AncientSwordRage Mar 02 '15 at 13:01
  • @warath-coder and Ruddra, becsuder of how many fields I end up with it bombs my DB. Do you have any refinements to this answer? – AncientSwordRage Mar 09 '15 at 09:08
  • 1
    @Pureferret Hi, as you can see, I think using M2M isn't the problem here. But querying M2M could require optimisation. So, I think you can look into this: https://docs.djangoproject.com/en/1.7/ref/models/querysets/#django.db.models.query.QuerySet.prefetch_related – ruddra Mar 09 '15 at 09:28
  • I've looked at those, and I'm not sure where best to place them. I'm using properties to get back a lot of the Attributes and Skills - when I try to get a mage from the db on the console, it seems to come with those links baked in already. – AncientSwordRage Mar 09 '15 at 10:03
2

Why don't you just work with a ManyToManyField?

That way you can easily add and manage those attributes. Also more easy to manage, as in your situation you have to add a column for each attribute.

phoenix
  • 7,988
  • 6
  • 39
  • 45
2

Ok, I'll be blamed to death but there is also the MonkeyPatch technique (which I like and use but can lead to poor readability) or you can make a simple shortcut.

Here the monkey patch, the best part of it is that if you have a skill index somewhere, you can use it:

SKILLS = ["skill_1", "skill_2", "skill_3"]
for skill in SKILLS:
    IntegerRangeField(
        min_value=min_value, max_value=max_value
    ).contribute_to_class(MyModel, skill)

Note: it works well with migrations while you keep it in the right app models files

Here the shortcut:

def new_skill(min_value=1, max_value=10, **kwargs):
    return IntegerRangeField(
        min_value=min_value, max_value=max_value, **kwargs)

class MyModel(models.Model):
    skill_1 = new_skill()
    skill_2 = new_skill()
    skill_3 = new_skill(verbose_name=_("translatable skill name"))

Hope it can helps.

edit: You suggested to split the repetitive ugly part listing all fields in an other file so new possibility, hide hugly not dry code in an abstract class (or split models.py but it's harder to explain).

class RpgSkills(models.Model):
    skill_1 = new_skill()
    skill_2 = new_skill()
    skill_3 = new_skill(verbose_name=_("translatable skill name"))

    class Meta:
        abstract = True # means will not have any data table by it's own

Then in your models.py:

from skills_utils import RpgSkills

class Character(RpgSkills):
    name = CharField(max_length=128)

Here more about abstract classes : https://docs.djangoproject.com/en/1.7/topics/db/models/#abstract-base-classes

Or you can combine all together if you are a funny man:

here the tool:

def skill_fields(*skills):
    class MyAbstractModel(models.Model):
        class Meta:
            abstract = True

    for skill in skills:
        new_skill().contribute_to_class(MyAbstractModel, skill)
    return MyAbstractModel

here the usage:

from skills_utils import skill_fields

class Character(skill_fields("skill_1", "skill_2", *settings.MY_APP_SKILL_LIST)):
    name = CharField(max_length=128)

But this last is for those who have the finest humour only ;-)

PS: If I were you I would choose the shortcut way which leave open more customisation if you want to have some special skills with wider or smaller range and it's simply more readable for who ever can take the project after you.

christophe31
  • 6,359
  • 4
  • 34
  • 46
  • This certainly gets round my dry problem, but for some reason it doesn't feel 'right'. – AncientSwordRage Mar 12 '15 at 15:07
  • 1
    which one? The MonkeyPatch way of doing? I do this some time to avoid circular dependencies or to upgrade models from dependencies apps (so I can upgrade them later). If you are in the same model file, it's not that bad. And with django migration in your pocket it's even rather cool, you can update your skill index and make your schema migration. But I agree, never show your code here after, or you'll get persons threatening your life for offence to the holy pythonic way of doing things ;-). – christophe31 Mar 12 '15 at 16:40
  • Either! Too scared of offending the Knights of Pythonia. – AncientSwordRage Mar 12 '15 at 16:52
  • 1
    the shortcut one seems to me as a good practice, it would allow you to update globally your default and is a quick/readble way to define your default field (wich is in this case simpler than making a SkillField inheritting IntegerRangeField to just pass default values) – christophe31 Mar 12 '15 at 17:03
  • It might be good with having just a simple model to collect the 'shortcut fields'...and bung that in a different file. – AncientSwordRage Mar 12 '15 at 17:08
  • You mean an abstract model? Indeed, it can free up your models.py, I add it to my answer. – christophe31 Mar 12 '15 at 17:36
  • thanx for the bounty, I'm over 2 thousands! – christophe31 Mar 13 '15 at 09:42
1

It depends on how you're going to use these stats.

  • If you need to get the list of players based on their intelligence for example; having a field for it is easier/better (your solution). That solution also involves less code (at least initially).

  • If you need to list statistics, see the best characters for each, etc. you better go with the solution you linked. Ask yourself, should "intelligence" be an "entity" on its own (with a slug, a description, etc.)?

  • Here is another option; let's say you just need a global "level" value for most queries and the details of the stats are only useful when checking a player:

-

from jsonfield.fields import JSONField


class User(…):
    statistics = JSONField(help_text="Intelligence, Wits, etc.")
    level = models.PositiveIntegerField(default=0)

This way you can pack your 30+ stats in one field and still query quickly on the user level/experience. You could also extend JSONField.

François Constant
  • 5,531
  • 1
  • 33
  • 39
  • Thanks for your useful advice. I've updated my question. I'm curious about your code snippet; did you mean to write 'user' there? Or am I misunderstanding how you're suggesting I use it? – AncientSwordRage Jan 27 '15 at 12:54
  • 1
    I assumed you were recording theses stats for each players (= user). The JSONField is an alternative if you don't need to query on specific skills (like intelligence) but just need to record these details. – François Constant Jan 27 '15 at 22:30
  • hstore field would be a better field type then this jsonField (if the backend is postgres), but the best answer is @ruddra's – Below the Radar Mar 10 '15 at 13:51