Swynfel / ceramic

Environment to play variations of the board game Azul, usable for Reinforcement Learning research in C++ and Python
GNU General Public License v3.0
10 stars 2 forks source link

Can't run Python's documentation example #2

Open RemiFabre opened 7 months ago

RemiFabre commented 7 months ago

When trying to run this code:

import random

from ceramic.game import Action, Game, GameHelper, Player
from ceramic.players import RandomPlayer
from ceramic.rules import Rules
from ceramic.state import Tile

class TestPlayer(Player):
    def __init__(self):
        Player.__init__(self)

    def play(self, state):
        special_action = Action(1, Tile.from_letter("B"), 3)
        if GameHelper.legal(special_action, state):
            return special_action
        legal_actions = GameHelper.all_legal(state)
        return random.choice(legal_actions)

game = Game(Rules.BASE)
game.add_player(TestPlayer())
game.add_players([RandomPlayer() for i in range(0, 3)])
game.roll_game()  # Plays a random game until the end
print("The winner is:", game.state.winning_player())
print(f"Game state: {game.state}")

I get this error:

    game.roll_game()  # Plays a random game until the end
RuntimeError: Tried to call pure virtual function "Player::play"

When calling "roll_round", the code runs but the state is empty, e.g:

Score: 0
[abcde] [ ]
[eabcd] [  ]
[deabc] [   ]
[cdeab] [    ]
[bcdea] [     ]
Floor: 0 (-0)

tox did work (although I'm using python 3.10). Any ideas about what is happening? Maybe the Python binding is failing?

Swynfel commented 7 months ago

Hello (again)!

Ok, this is weird, and if the tests worked it means tests/game/test_game.py#L58 worked, which should test exactly this. It's probably due to weird a behaviour of pybind11? Maybe things changed in the last years?

It might be that TestPlayer() is destroyed in python once going into add_player (a c++ method), and then the reference in c++ doesn't know it had overriden methods, unlike in the test where it keeps a reference in python (python_random_player = PythonRandomPlayer())...

I will investigate

Swynfel commented 7 months ago

So, this came from a very interesting behaviour of pybind11 (most information can be found in #pybind/pybind11/pull/2839), and indeed the work-around is to store all python players in python while they are used in a game. Namely:

import random

from ceramic.game import Action, Game, GameHelper, Player
from ceramic.players import RandomPlayer
from ceramic.rules import Rules
from ceramic.state import Tile

class TestPlayer(Player):
    def __init__(self):
        Player.__init__(self)

    def play(self, state):
        special_action = Action(1, Tile.from_letter("B"), 3)
        if GameHelper.legal(special_action, state):
            return special_action
        legal_actions = GameHelper.all_legal(state)
        return random.choice(legal_actions)

game = Game(Rules.BASE)
test_player = TestPlayer() # <-- here
game.add_player(test_player)
game.add_players([RandomPlayer() for i in range(0, 3)])
game.roll_game()  # Plays a random game until the end
print("The winner is:", game.state.winning_player())
print(f"Game state: {game.state}")

I will try to implement a better work-around directly in c++ following the comments in the thread, or take the not-yet-merged "smart_holder" branch of pybind11 in the building pipeline (will be easier to do once moved to pyproject.toml)

RemiFabre commented 7 months ago

It works, great! Thanks a lot