0

Context: I'm making a game that happens in a maze made of square tiles and almost everything, from movement to attacks, involves directions, which are mostly used to index lists. Subtracting or adding to directions is an easy way to turn left or right, but I always have to check that they are still within bounds, and I would like to automate that by making a custom class.

Here is how I'm currently doing it:

global UP
UP = 0
global RIGHT
RIGHT = 1
global DOWN
DOWN = 2
global LEFT
LEFT = 3

And here is what I'd like to do:

class Direction:
    number_of_directions=4

    def __init__(self,direction):
        self.direction = direction

    def __int__(self):
        return self.direction

    def __add__(self,other): #Here other is supposed to be an int
        return (self.direction + other)%number_of_directions

    def __sub__(self,other): #Here other is supposed to be an int
        return (self.direction - other)%number_of_directions

global UP
UP = Direction(0)
global LEFT
LEFT = Direction(1)
global DOWN
DOWN = Direction(2)
global RIGHT
RIGHT = Direction(3)

The only problem with that is that I am using UP, RIGHT, etc. as indexes, like I have a Tile that has a list of four Wall and I constantly call Tile.walls[direction] with direction being one of my four constants, and I don't want to have to specify Tile.walls[int(direction)] everytime.

Is there a way to have direction automatically coerced into an int whenever it's used for indexing?

wjandrea
  • 28,235
  • 9
  • 60
  • 81
M_R-B
  • 48
  • 1
  • 6
  • Can you make your class a subclass of `int`? – Barmar Apr 16 '22 at 05:56
  • @Barmar works perfectly! I don't even need my class to have an __int__() method. Which leaves me wondering how it decides what value to index with and how that would work with objects that have several attributes... I'll also look into IntEnum but if you make your comment into an answer I'll accept it. – M_R-B Apr 16 '22 at 06:34
  • I haven't worked out the details myself. If you got it working yourself, post your own answer. – Barmar Apr 16 '22 at 15:12
  • Beside the point, but declaring `global` in the global scope is redundant – wjandrea Apr 17 '22 at 18:16

3 Answers3

1

you could use an IntEnum:

from enum import IntEnum
from numbers import Integral

class Direction(IntEnum):
    UP = 0
    RIGHT = 1
    DOWN = 2
    LEFT = 3
    _NB_DIRECTIONS = 4

    def __add__(self, other):
        if isinstance(other, Integral):
            return Direction((self.value + other) % Direction._NB_DIRECTIONS)
        return NotImplemented

    def __sub__(self, other):
        if isinstance(other, Integral):
            return Direction((self.value - other) % Direction._NB_DIRECTIONS)
        return NotImplemented

those are subclasses of int and can be used e.g. as indices for lists:

lst = list(range(4))
print(lst[Direction.LEFT])  # -> 3

the examples you give work like this:

print(Direction.UP)                            # Direction.UP
print(Direction.UP + 1)                        # Direction.RIGHT
print(Direction.UP - 1)                        # Direction.LEFT
print(Direction.UP + 10)                       # Direction.DOWN
a = Direction.UP
a += 1
print(a)                                       # Direction.RIGHT
print(Direction.UP)                            # Direction.UP
print(type(a))                                 # <enum 'Direction'>
b = 1
print(type(b))                                 # <class 'int'>
b += Direction.UP
print(b)                                       # 1
print(type(b))                                 # <class 'int'>
print(Direction.DOWN - 1 == Direction.UP + 1)  # True
lst = ["zero", "one", "two", "three"]
print(lst[Direction.DOWN])                     # 'two'
print(lst[Direction.UP + 3])                   # 'three'
print(lst[Direction.LEFT - 2])                 # 'one'
hiro protagonist
  • 44,693
  • 14
  • 86
  • 111
  • Did you mean *raise NotImplemented*? – DarkKnight Apr 16 '22 at 06:22
  • 1
    @LancelotduLac no. `return NotImplemented` allows for the following: if `other` is of some class `NewDirection` (which `Direction` may not know about) and it should be possible to add objects of the two classes, then `NewDirection` can still implement `__radd__` and `Direction.LEFT + NewDirection.DOWN` will work (no exception handling needed). or see here: https://stackoverflow.com/questions/878943/why-return-notimplemented-instead-of-raising-notimplementederror#879005 – hiro protagonist Apr 16 '22 at 06:29
0

I got my code to work by making Direction a subclass of int.

Here is what my current code looks like:

global NB_DIRECTIONS
NB_DIRECTIONS = 4

(This part is a bit superfluous, just in case I want to adapt the game with, say, hexagons or triangles instead of squares later on. The real code starts now.)

class Direction(int):
    directions = NB_DIRECTIONS
    def __init__(self,direction):
        self.direction = direction

    def __add__(self,other):
        if isinstance(other,int):
            return Direction((self.direction+other)%self.directions)
        return NotImplemented

    def __radd__(self,other):
        if isinstance(other,int):
            return Direction((other+self.direction)%self.directions)
        return NotImplemented

    def __sub__(self,other):
        if isinstance(other,int):
            return Direction((self.direction-other)%self.directions)
        return NotImplemented

    def __rsub__(self,other):
        return NotImplemented

    def __eq__(self, other):
        if isinstance(other,Direction):
            return self.direction == other.direction
        return NotImplemented

global UP
UP=Direction(0)
global RIGHT
RIGHT=Direction(1)
global DOWN
DOWN=Direction(2)
global LEFT
LEFT=Direction(3)

(I made the addition be between a Direction object and an int, not between two Direction object, because it makes more sens for what I'm doing, but that's irrelevant to the problem of indexing I was trying to solve.)

Let's look at my Direction's behavior:

>>> UP
0
>>> UP+1
1
>>> UP-1
3
>>> UP+10
2
>>> a=UP
>>> a+=1
>>> a
1
>>> UP
0
>>> type(a)
<class '__main__.Direction'>
>>> b=1
>>> type(b)
<class 'int'>
>>> b+=UP
>>> b
1
>>> UP
0
>>> type(b)
<class '__main__.Direction'>
>>> DOWN-1==UP+1
True
>>> lst=["zero","one","two","three"]
>>> lst[DOWN]
'two'
>>> lst[UP+3]
'three'
>>> lst[LEFT-2]
'one'
>>> type(RIGHT)
<class '__main__.Direction'>

TL;DR: I made my class inherit from int, now it can be used as an index.

M_R-B
  • 48
  • 1
  • 6
  • This is working mostly by coincidence. The actual integer value of your class, as used in indexing, was set at the creation of the instance (via the `__new__` method that you didn't override), and has absolutely nothing to do with your `direction` attribute. – jasonharper Apr 17 '22 at 18:25
0

Yes, simply define __index__(). For example:

class Direction:

    def __init__(self, direction):
        self.direction = direction

    def __index__(self):
        return self.direction

UP = Direction(0)
cardinals = ['north', 'east', 'south', 'west']
print(cardinals[UP])  # -> north

This also makes __int__ unnecessary since __index__ is used as a fallback.

print(int(UP))  # -> 0

P.S. For this answer, I'm ignoring any design considerations. Using an IntEnum might be a better solution, I'm not sure.

wjandrea
  • 28,235
  • 9
  • 60
  • 81