0

I am extending an existing PlayerClassFromGameEngine class to allow custom effects to take effect only for a certain duration.

Example: Using the original class, I would freeze a player by saying player.move_type = MoveTypes.Freeze and then unfreeze him by saying player.move_type = MoveTypes.Normal. Now I'd like to extend the class so I can use a function call instead: player.freeze(5), to freeze the player for five seconds.

I obviously need two functions, the effect function and an undo function, f.e. freeze() and unfreeze(). Here's my current class, that works fine:

class MyPlayer(PlayerClassFromGameEngine):
    def __init__(self, index):
        super().__init__(index)  # Let the original game engine handle this
        self.effects = defaultdict(set)

    def freeze(self, duration):
        self.move_type = MoveType.FREEZE  # Move type comes from the super class
        thread = threading.Thread(target=self._unfreeze)
        thread.args = (duration, thread)
        self.effects['freeze'].add(thread)
        thread.start()

    def _unfreeze(self, duration, thread):
        time.sleep(duration)
        self.effects['freeze'].remove(thread)
        if not self.effects['freeze']:  # No more freeze effects
            self.move_type = MoveType.NORMAL

As you see, only one effect takes more than 10 lines of code, having 20 of these would be awful, since they all work the exact same way, just with different key ('freeze', burn, etc.) and some call a function instead of accessing move_type property.

I've got basically zero idea where to start, maybe descriptors and decorators somehow, but can somebody give me some advice, or better yet a working solution?


EDIT: Here's what I came up with after Martijn's suggestion, but it doesn't work since I can't access the player inside the Effect class

from collections import defaultdict
from threading import Thread
from time import sleep

class Effect(object):
    def __init__(self, f, undo_f=None):
        self.f = f
        self.undo_f = undo_f
        self._thread = None

    def __call__(self, duration):
        self._thread = Thread(target=self._execute, args=(duration, ))
        self._thread.start()

    def _execute(self, duration):
        self.f()
        sleep(duration)
        self.undo_f()

    def undo(self, undo_f):
        return type(self)(self.f, undo_f)

class Player:
    def __init__(self, index):
        self.index = index
        self._effects = defaultdict(set)

    @Effect
    def freeze(self):
        print('FROZEN')

    @freeze.undo
    def freeze(self):
        print('UNFROZEN')

p = Player(1)
p.freeze(3)

What I think I need is to somehow access the player inside of the Effect class, since I can't call self.f(player) or self.undo_f(player) in the Effect._execute method, nor can I access player's effects dictionary. I figured I won't be needing the key parameter anywhere, since I can just generate a random number for every effect (an unique one ofc.), since it's not shown to anyone anyways.

Markus Meskanen
  • 19,939
  • 18
  • 80
  • 119
  • Your decorators are *just fine*; why do you think they'd be impossible? All your `@effect()` decorator needs to do is to also register the un-freeze. You could do that with `@freeze.undo` as the decorator on another `def freeze()` method, just like `@property` and `@propertyname.setter`; see [How does the @property decorator work?](https://stackoverflow.com/q/17330160) for how that works. – Martijn Pieters Mar 10 '15 at 21:22
  • @MartijnPieters There's no way to connect the two methods together so that the proper `thread` would be removed from the `effects` dictionary, at least I couldn't come up with a way. I've never used descriptors and I got only little knowledge with decorators, maybe I just can't come up with a solution.. Which is why I'm asking for help in the first place :> – Markus Meskanen Mar 10 '15 at 21:26
  • I'm out of time right now to write this out to a full answer; but I suggest you read up on how the `property` decorator works (see the link in my earlier comment) and take it from there. – Martijn Pieters Mar 10 '15 at 21:28
  • @MartijnPieters Okay I came up with something, it's not a working solution but I think I'm on the right track. I edited the question so you can see what I've got, but I'd love to get some help if you ever got some free time. :) I have got to go to bed now, but I'll check here daily – Markus Meskanen Mar 10 '15 at 22:35
  • You are nearly there, but missing a crucial bit - read up on the descriptor protocol. That's what @MartijnPieters meant when talking about the property decorator. With that, your Effec-class implements a __get__ method which will get passed the current player instance for that specific call. On a related note: I think you should reconsider using threads for your approach, and instead use the gameengine/system/libraries' event/timer system instead. Otherwise, the threads will be plenty, and the problems as well. – deets Mar 10 '15 at 23:29
  • @deets Still not sure how to do it :/ I'm going to use the engine's system, but I find it much easier to learn the stuff in small mini programs with simple threading etc. and then apply it to my actual program. – Markus Meskanen Mar 11 '15 at 13:22
  • @MartijnPieters Any suggestions? Is deets' answer similar to what you were thinking about? – Markus Meskanen Mar 11 '15 at 17:45
  • @MarkusMeskanen: similar, but with some problems in the implementation. – Martijn Pieters Mar 11 '15 at 17:50
  • @MarkusMeskanen: implementation problems corrected, that looks like what I meant, yes. – Martijn Pieters Mar 11 '15 at 21:23

1 Answers1

2

This would be a way to go:

from functools import partial
import time
import threading


class aftereffect(object):
    def __init__(self, func):
        self._func = func
        self._aftereffect = lambda instance: None


    def aftereffect(self, func):
        self._aftereffect = func
        return self


    def __get__(self, instance, cls):
        # this is the descriptor protocol, and
        # instance is the actual object

        def delayed_effect(timeout):
            time.sleep(timeout)
            self._aftereffect(instance)

        def caller(*args, **kwargs):
            timeout = kwargs.pop("_timeout", 1.)
            t = threading.Thread(target=partial(delayed_effect, timeout=timeout))
            t.start()
            return self._func(*args, **kwargs)

        return caller.__get__(instance, cls)


class Thing(object):
    @aftereffect
    def something(self):
        print "something", self

    @something.aftereffect
    def something(self):
        print "after_something", self


t = Thing()
t.something()
t.something(_timeout=5)
Martijn Pieters
  • 1,048,767
  • 296
  • 4,058
  • 3,343
deets
  • 6,285
  • 29
  • 28
  • This doesn't allow me to use a custom `duration` for each call. I'd like to do something like `t.something(5)` instead of a static sleep value. – Markus Meskanen Mar 11 '15 at 15:47
  • As mentioned, it doesn't allow me to use a custom duration for each **call**. I don't always want to freeze the player for 5 seconds, I sometimes want to freeze him for 3 and sometimes for 30. – Markus Meskanen Mar 11 '15 at 16:04
  • Misunderstood that, now it's on a per-call-base – deets Mar 11 '15 at 16:15
  • Still not sure, seems rather hacky... Defining two functions in one function just to access an other function. I'll wait for more answers, but it's better than nothing :) – Markus Meskanen Mar 11 '15 at 16:18
  • Defining local functions is nothing spectacular in python (or in other languages for that matter). But let the inrush of alternative implementations come... – deets Mar 11 '15 at 16:41
  • Nice catch. The namespace pollution could obviously still occur (as with property.setter) - but at least now it can be prevented ;) – deets Mar 11 '15 at 18:01
  • @deets: which is why you use the same for the setter function! :-) – Martijn Pieters Mar 11 '15 at 21:21
  • I'd give the chained decorator a different name though; `@something.finalize` perhaps, or `@something.callback`. – Martijn Pieters Mar 11 '15 at 21:27
  • Okay I still don't like how it looks, but that's just me and it works better than I expected! I also spent a lot of time trying to understand how and why it works, and I think I got it now :) Also, since you both seem to agree so well on it, no reason why I wouldn't use it just because "it looks hacky" :D Thanks for the solution :) – Markus Meskanen Mar 11 '15 at 21:36
  • @MartijnPieters Sure, that's what you should do. It's just a pity that there is no solution that actually prevents you from making a mistake. In pre-property.setter-times, one could use an apply-based mechanism to that end, but it also was a tad bit clunky... – deets Mar 11 '15 at 21:38
  • @deets Why do I have to use partial here, why doesn't `t = threading.Thread(target=delayed_effect, args=(timeout,)` work? – Markus Meskanen Mar 11 '15 at 22:07
  • You don't have to use partial, I'm just used to curry arguments using partial regrardless of usecase. args=... doesn't exist in e.g. a map, filter or reduce argument, or similar cases where callables are required. IMHO (but that's a matter of taste solely) the rather wartish threading API shouldn't even offer this. You might prefer it - but e.g. your real game-loop, without threads, does that allow for args, or would that better work with a simple callable + timetou, e.g. Tkinters after-function? – deets Mar 11 '15 at 22:17
  • @deets The game allows the following: `delay( seconds, callback, *args, **kwargs)`, so I guess I'll be replacing the threading with `delay(timeout, delayed_effect, *args, **kwargs)` :) – Markus Meskanen Mar 11 '15 at 22:23
  • @deets https://gist.github.com/MarkusMeskanen/d43a4a85dba1c7e670ee Here's what I came up with, does it look ok? I'll add `*args, **kwargs` later on when I'm sure it all works as intended. It gets a little messy since I'm using both `instance` and `player` in the same function... – Markus Meskanen Mar 11 '15 at 22:32
  • It looks a bit downgraded through simplification, and confusing. As you say yourself - player & instance together look tautological, and maybe even produce an error as you bind instance as self through the __get__ (from my example), but then call self.f(player, duration) which should lead to 3 arguments passed (and Python complaiing). Also IMHO the duration on the freeze.undo is superfluous, at least from my understanding - the undo by itself is not really concerned with the duration, but gets it passed. – deets Mar 11 '15 at 22:46