3
\$\begingroup\$

So, there is this king-of-the-hill challenge going on at PPCG SE to which I submitted my participation. I won't change it further in terms of behaviour, so I wanted to know what you think of the code.

The challenge

It is very well described in the original post, I'll just summarize the key ideas here.

Setup

  • A pile of \$C\$ unflipped coins at the center of the table;
  • \$N\$ players around the table with, each, \$F_i\$ flipped coins and \$U_i\$ unflipped coins (\$i \in [0;N[\$)

Input of the program

Single argument formated following:

$$R;I;C;P_i\_S_i\_F_i\_U_i;P_i\_S_i\_F_i\_U_i;\ldots$$

Where \$R\$ is the current round, \$I\$ is the id of our player, \$P_i\$ is the id of the player being described by the \$P\_S\_F\_U\$ data and \$S_i\$ is its score.

Expected output

A 3-character string, nothing else, representing the actions of our player for this turn. The actions can be:

N: Do Nothing
1: Take 1 coin from the central pile [Effects: +1 local unflipped / -1 point / -1 global unflipped]
2: Take 2 coins from the central pile [Effects: +2 local unflipped / -2 points / -2 global unflipped]
3: Take 3 coins from the central pile [Effects: +3 local unflipped / -3 points / -3 global unflipped]
A: Put back 1 coin from your pile [Effects: -1 local unflipped / +1 point / +1 global unflipped]
B: Put back 2 coins from your pile [Effects: -2 local unflipped / +2 points / +2 global unflipped]
C: Put back 3 coins from your pile [Effects: -3 local unflipped / +3 points / +3 global unflipped]
X: Remove 1 coin from your pile [Effects: -1 local unflipped / 0 point]
Y: Remove 2 coins from your pile [Effects: -2 local unflipped / 0 point]
Z: Remove 3 coins from your pile [Effects: -3 local unflipped / 0 point]
R: Rotate coins to previous player [Effects: -1 point per unflipped received, +2 points per flipped received / applies to all players]
T: Rotate coins to next player [Effects: -1 point per unflipped received, +2 points per flipped received / applies to all players]
F: Flip 1 coin [Effects: -1 local unflipped / +1 local flipped / +2 point]
U: Unflip 1 coin [Effects: +1 local unflipped / -1 local flipped / -2 point]

The code

The submission tries every one of the 2744 possible move and output the (first) one with the maximum amount of points gained.

import sys
import itertools
from copy import deepcopy


MOVES_REQUIRED = 3

FLIPPED = 0
UNFLIPPED = 1


def filter_neighbors(neighbors, me, size):
    limit = size - MOVES_REQUIRED
    for data in neighbors:
        i, _, flipped, unflipped = map(int, data.split('_'))
        if MOVES_REQUIRED < (me - i) % size < limit:
            continue  # Skip neighbors that are too far away
        yield i, [flipped, unflipped]


class Player:
    def __init__(self, raw_data):
        _, me, coins, *data = raw_data.split(';')

        self.num_players = len(data)
        self._me = int(me)
        self._coins = int(coins)
        self._state = dict(filter_neighbors(data, self._me, self.num_players))

    def reset(self):
        self.me = self._me
        self.coins = self._coins
        self.state = deepcopy(self._state)
        self.my_state = self.state[self.me]

    def invalid_move(self, move):
        if move in 'NRT':
            return False

        if move in '123'[:self.coins]:
            return False

        flipped, unflipped = self.my_state
        if flipped and move == 'U':
            return False
        if unflipped and move == 'F':
            return False

        if move in 'AXBYCZ'[:2 * unflipped]:
            return False

        return True

    def N(self):
        return 0

    def one(self):
        self.coins -= 1
        self.my_state[UNFLIPPED] += 1
        return -1

    def two(self):
        self.coins -= 2
        self.my_state[UNFLIPPED] += 2
        return -2

    def three(self):
        self.coins -= 3
        self.my_state[UNFLIPPED] += 3
        return -3

    def A(self):
        self.coins += 1
        self.my_state[UNFLIPPED] -= 1
        return 1

    def B(self):
        self.coins += 2
        self.my_state[UNFLIPPED] -= 2
        return 2

    def C(self):
        self.coins += 3
        self.my_state[UNFLIPPED] -= 3
        return 3

    def X(self):
        self.my_state[UNFLIPPED] -= 1
        return 0

    def Y(self):
        self.my_state[UNFLIPPED] -= 2
        return 0

    def Z(self):
        self.my_state[UNFLIPPED] -= 3
        return 0

    def R(self):
        self.me = (self.me + 1) % self.num_players
        flipped, unflipped = self.my_state = self.state[self.me]
        return 2 * flipped - unflipped

    def T(self):
        self.me = (self.me - 1) % self.num_players
        flipped, unflipped = self.my_state = self.state[self.me]
        return 2 * flipped - unflipped

    def F(self):
        self.my_state[FLIPPED] += 1
        self.my_state[UNFLIPPED] -= 1
        return 2

    def U(self):
        self.my_state[FLIPPED] -= 1
        self.my_state[UNFLIPPED] += 1
        return -2

setattr(Player, '1', Player.one)
setattr(Player, '2', Player.two)
setattr(Player, '3', Player.three)


def scenarii(player):
    for tries in itertools.product('N123ABCXYZRTFU', repeat=MOVES_REQUIRED):
        player.reset()
        points = 0
        for try_ in tries:
            if player.invalid_move(try_):
                break
            points += getattr(player, try_)()
        else:
            yield points, ''.join(tries)


if __name__ == '__main__':
    player = Player(sys.argv[1])
    print(max(scenarii(player))[1])

What do you think?

\$\endgroup\$
  • \$\begingroup\$ Yo, I noticed this is your only unaccepted question. Is there something the answer is missing that you want answered is particular? If so I may be able to help, :) \$\endgroup\$ – Peilonrayz Jun 26 '17 at 20:46
  • \$\begingroup\$ @Peilonrayz It's just that, as stated in the comments, I really dislike the hit on readability of the current answer. Otherwise, I don't have any particular wish, any and all aspects, as usual ;) \$\endgroup\$ – 301_Moved_Permanently Jun 27 '17 at 10:33
1
\$\begingroup\$

I'm going to expound on this answer later (when I'm on my Python 3 machine). To begin with, there's a lot of repeated logic, especially in the one, two, ... C methods. You can avoid this with a class method to generate these instance methods.

class Player:

    @classmethod
    def make_flipper(cls, name, val):
        def flipper(self):
            self.coins -= val
            self.my_state[UNFLIPPED] += val
            return -val
        setattr(cls, name, flipper)

    method_dict = {
        'N': 0,
        'one': 1,
        'two': 2,
        'three': 3,
        'A': -1,
        'B': -2,
        'C': -3
    }

    for name, val in method_dict.items():
        make_flipper(name, val)

Note that this is untested, I'll fix it when I get home if it's broken.

| improve this answer | |
\$\endgroup\$
  • \$\begingroup\$ I do agree that we can do something to reduce the amount of repeated code, but I don't find this solution that readable. Plus the help on the class will be messed up, if we ever need to add docstrings... \$\endgroup\$ – 301_Moved_Permanently May 5 '16 at 7:25
  • \$\begingroup\$ @MathiasEttinger I agree with the readability comment. Regarding the docstring, this can be done programmatically on the wrapper function by setting __doc__ IIRC. I'll try to think of another approach though, didn't end up having any time last night. \$\endgroup\$ – Jared Goguen May 5 '16 at 15:34

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.