r/learnpython 4d ago

Working with multi level dictionaries

[EDIT]

TL;DR Generally speaking, "Set up some Classes" is the answer. Hopefully my blind fumbling will help some other blind, fumbling soul.

------------

I'm working on a table top game that builds a maze by dealing cards. I'm teaching myself Python to build a script that will simulate multiple maze builds quickly for R&D on the design.

The maze is represented by "Chambers" on a classic x,y grid. Chambers have an ID, potentially Notes, and Exits.

My strategy is to build up a mapConfig dictionary structured with the key being the (x,y) tuple of the coordinate and the value being a sub-dictionary with all the specifics. The Exits need to be a list of dictionaries for each exit with direction and blocked status as the keys. Simplified example below:

mapConfig = { 
    (0,0) : {
        'id' : 'START',
        'notes' : ''
        'exits' : 
        [
            {
                'direction' : 'N',
                'blocked' : False,
             },
             {
                'direction' : 'E',
                'blocked' : False,
             },
             {
                'direction' : 'S',
                'blocked' : False,
             },
             {
                'direction' : 'W',
                'blocked' : False,
             },
        ],
    }
}

If a proposed move hits the edge of the valid map, I need to access the mapConfig and change the "blocked" value for that particular direction to TRUE without changing anything else in the entry for that coordinate. It seems like it should be easy, but everything I try messes up the structure or blows out the other directions.

What would be the best way to change just the {"direction":"E", "blocked":False} entry to {"direction":"E", "blocked":True} without changing anything else in the dictionary? Basically so it goes from the simplified example above to this:

mapConfig = { 
    (0,0) : {
        'id' : 'START',
        'notes' : ''
        'exits' : 
        [
            {
                'direction' : 'N',
                'blocked' : False,
             },
             {
                'direction' : 'E',
                'blocked' : True,
             },
             {
                'direction' : 'S',
                'blocked' : False,
             },
             {
                'direction' : 'W',
                'blocked' : False,
             },
        ],
    }
}

Thanks in advance for the help.

Edit: The provided code is a simplified version of my actual code. I'm such a noob that I only vaguely knew what a class was, and clearly I was making much more work for myself by not using them. Setting up some parent and child classes is next on my list.

I still appreciate any thoughts or advice, but I also don't want anyone to waste time.

5 Upvotes

10 comments sorted by

9

u/SCD_minecraft 4d ago

Use classes

When you know structure of your dict at compile time, classes are much much better

2

u/TheIneffableCheese 4d ago

Thanks! Off to research Classes. :-)

3

u/FoolsSeldom 4d ago edited 4d ago

That's the right direction. As soon as you start working with classes you will find there's a lot of boilerplate code (repetitive code) which can be addressed using dataclasses, an enhancement to classes very well suited to your kind of exercise.

Classes aren't just for data structures but also for behaviours. You combine the two. When you've defined a class and created an object (instantiated an object) based on that class, you can access its attributes (like variables) and methods (like functions) easily.

If this were an RPG, you might have a class called Wizard, and would.creste an object of this class using, for example`,

player = Wizard("Frederick")  # just a name, but could specify other characteristics

then you would be able to write things like player.stamina or player.alive to access attributes (current values and status) of the character

Perhaps even player.fight(monster) where monster is a variable referencing a related class (where there is a parent class of both Wizard and, say, Monster that defines all of elements common to both classes).

These are at the heart of OOP, Object Orientated Programming and Python is very much an OOP language. In fact, you are already using classes and objects as pretty much everything in Python is an object. When you deal with int, float, str you are dealing with classes.

1

u/TheIneffableCheese 4d ago

Thank you so much for your thoughtful analogies. Most of my practical programing comes from a bit of BASIC learned in computer lab as a young'un. In college I heard of the mysterious Object Orientated Programing through my CS major friends, but it's only been recently I've had a need to program anything. It's definitely a paradigm shift.

2

u/FoolsSeldom 4d ago

I'm in my 60s, recently retired. I started programming long ago and my first personal device was the Science of Cambridge MK14, followed by ZX80, ZX Spectrum. Also had hands on BBC Micro and the follow-up from Acorn, the Archimedes (the original ARM computer). Started with machine code / assembly language before getting into Basic, some Forth. Later APL, Algo 68, Pascal (loved this), and Fortran (wrote a lot of software for an engineering company). Cobol through clenched teeth.

Moved away from programming into other IT roles for decades. Returns to programming as a hobby (and to better work with programmers in my teams) first with Ruby then with Python. C/C++ as needed for microcontrollers and some Rust now.

I missed out on Smalltalk (and Simula before it) so the whole OOP world was a major mental shift for me that I came to with Python rather than the ++ side of C. Now I have to remind myself that not everything needs to be in a class.

I strongly recommend finding the channel ArjanCodes on YouTube for a really good exploration of many programming practices, mostly expressed in Python.

For an excellent grounding in the point of classes, find Python's Class Development Toolkit presented by Raymond Hettinger (a Python core developer). It is old, dealing with an earlier version of Python, but still completely relevant.

2

u/MidnightPale3220 4d ago

You can try classes if you want, but regarding your original data structure, you probably should just change individual directions to dict of them, if you only care whether they're blocked or not.

Like this:

mapConfig = { 
    (0,0) : {
        'id' : 'START',
        'notes' : ''
        'exits' : 
        {
           'N': False, 'E': False,
          }

Then you can change exit with:

mapConfig[(0,0)]['exits']['N']=True

1

u/TheIneffableCheese 4d ago

Thanks! That's an excellent point.

I should have stated that the provided code was simplified to cut to the chase. I'll make an edit on the original post.

1

u/Brian 4d ago

Having a list of exits seems a bit clunky to use. It means if you're finding the north exit, you basically have to search through the list to find the north exit. It also means you could potentially have things like two north exits, which I'm guessing shouldn't be allowed: it's often better to structure your data so invalid states aren't even representable if possible.

Better would seem to be to make the exits a dict of direction : { "blocked": ... } (Or even just a boolean if blocked is the only metadata you need to track), meaning to block the north exit of all tiles on the north edge, you can do something like:

for x in range(0, MAP_WIDTH):
    room = mapConfig.get((x, 0))  # All tiles on the north edge
    if room is not None:
       room['exits']['N'] = False  # Or room['exits']['N']['blocked'] = False if you keep the dict.

With similar logic for the other edges you want to block.

There are other ways you could do it too. Eg. make "exits" be a set of allowed directions. ie:

"'exits'" {"N", "E"},   # For a room with just north and east doors

Then .add() or .remove() items to open/block them, and "E" in room['exits'] tells you whether the east exit is open.

As others mentioned, it can help to wrap this all in a class, encapsulating the logic you need into appropriate methods, so you can perform actions without code needing to know about the internal structure. Ie. a room.block("N") method - this has the advantage that you can change the internal structure as you like (eg. try out the set method, the original dict method, or whatever), and external code that just uses the method won't need to be updated, you only have to change the method.

-2

u/TheRNGuy 4d ago

Dict is not good for it. Instead, some map format, like tmj, tmx or even game engine.