Skip to content

Commit bb6a644

Browse files
committed
an example of a cheating rdeep bot
1 parent d3f3cf2 commit bb6a644

File tree

4 files changed

+129
-3
lines changed

4 files changed

+129
-3
lines changed

executables/cli.py

+15-2
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
from schnapsen.bots import MLDataBot, train_ML_model, MLPlayingBot, RandBot
1010

1111
from schnapsen.bots.example_bot import ExampleBot
12+
from schnapsen.bots.rdeep_cheater import RdeepCheaterBot
1213

1314
from schnapsen.game import (Bot, GamePlayEngine, Move, PlayerPerspective,
1415
SchnapsenGamePlayEngine, TrumpExchange)
@@ -33,10 +34,10 @@ def play_games_and_return_stats(engine: GamePlayEngine, bot1: Bot, bot2: Bot, nu
3334
if i % 2 == 0:
3435
# swap bots so both start the same number of times
3536
lead, follower = follower, lead
36-
winner, _, _ = engine.play_game(lead, follower, random.Random(i))
37+
winner, _, _ = engine.play_game(lead, follower, random.Random(i + 100))
3738
if winner == bot1:
3839
bot1_wins += 1
39-
if i % 500 == 0:
40+
if i % 10 == 0:
4041
print(f"Progress: {i}/{number_of_games}")
4142
return bot1_wins
4243

@@ -109,6 +110,18 @@ def rdeep_game() -> None:
109110
print(f"won {wins} out of {game_number}")
110111

111112

113+
@main.command()
114+
def rdeep_cheater() -> None:
115+
engine = SchnapsenGamePlayEngine()
116+
#bot1: Bot = RdeepBot(10, 5, random.Random(4535), "fairplay")
117+
bot1: Bot = RdeepCheaterBot(10, 5, random.Random(4556), "cheater")
118+
bot2 = RandBot(random.Random(678473), "rand")
119+
number_of_games: int = 1000
120+
121+
bot1_wins = play_games_and_return_stats(engine=engine, bot1=bot1, bot2=bot2, number_of_games=number_of_games)
122+
print(f"{bot1} wins {bot1_wins} times out of {number_of_games} games played.")
123+
124+
112125
@main.group()
113126
def ml() -> None:
114127
"""Commands for the ML bot"""

src/schnapsen/bots/__init__.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -7,5 +7,6 @@
77
from .ml_bot import MLDataBot, MLPlayingBot, train_ML_model
88
from .gui.guibot import SchnapsenServer
99
from .minimax import MiniMaxBot
10+
from .rdeep_cheater import RdeepCheaterBot
1011

11-
__all__ = ["RandBot", "AlphaBetaBot", "RdeepBot", "MLDataBot", "MLPlayingBot", "train_ML_model", "SchnapsenServer", "MiniMaxBot"]
12+
__all__ = ["RandBot", "AlphaBetaBot", "RdeepBot", "MLDataBot", "MLPlayingBot", "train_ML_model", "SchnapsenServer", "MiniMaxBot", "RdeepCheaterBot"]

src/schnapsen/bots/rdeep_cheater.py

+96
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
from typing import Optional
2+
from schnapsen.game import Bot, PlayerPerspective, Move, GameState, GamePlayEngine
3+
import random
4+
5+
from .rand import RandBot
6+
7+
8+
class RdeepCheaterBot(Bot):
9+
"""
10+
Rdeep bot is a bot which performs many random rollouts of the game to decide which move to play.
11+
"""
12+
def __init__(self, num_samples: int, depth: int, rand: random.Random, name: Optional[str] = None) -> None:
13+
"""
14+
Create a new rdeep bot.
15+
16+
:param num_samples: how many samples to take per move
17+
:param depth: how deep to sample
18+
:param rand: the source of randomness for this Bot
19+
:param name: the name of this Bot
20+
"""
21+
super().__init__(name)
22+
assert num_samples >= 1, f"we cannot work with less than one sample, got {num_samples}"
23+
assert depth >= 1, f"it does not make sense to use a dept <1. got {depth}"
24+
self.__num_samples = num_samples
25+
self.__depth = depth
26+
self.__rand = rand
27+
28+
def get_move(self, perspective: PlayerPerspective, leader_move: Optional[Move]) -> Move:
29+
# get the list of valid moves, and shuffle it such
30+
# that we get a random move of the highest scoring
31+
# ones if there are multiple highest scoring moves.
32+
moves = perspective.valid_moves()
33+
self.__rand.shuffle(moves)
34+
35+
best_score = float('-inf')
36+
best_move = None
37+
for move in moves:
38+
sum_of_scores = 0.0
39+
for _ in range(self.__num_samples):
40+
gamestate = perspective.make_cheating_assumption(leader_move=leader_move, rand=self.__rand)
41+
score = self.__evaluate(gamestate, perspective.get_engine(), leader_move, move)
42+
sum_of_scores += score
43+
average_score = sum_of_scores / self.__num_samples
44+
if average_score > best_score:
45+
best_score = average_score
46+
best_move = move
47+
assert best_move is not None
48+
return best_move
49+
50+
def __evaluate(self, gamestate: GameState, engine: GamePlayEngine, leader_move: Optional[Move], my_move: Move) -> float:
51+
"""
52+
Evaluates the value of the given state for the given player
53+
:param state: The state to evaluate
54+
:param player: The player for whom to evaluate this state (1 or 2)
55+
:return: A float representing the value of this state for the given player. The higher the value, the better the
56+
state is for the player.
57+
"""
58+
me: Bot
59+
leader_bot: Bot
60+
follower_bot: Bot
61+
62+
if leader_move:
63+
# we know what the other bot played
64+
leader_bot = FirstFixedMoveThenBaseBot(RandBot(rand=self.__rand), leader_move)
65+
# I am the follower
66+
me = follower_bot = FirstFixedMoveThenBaseBot(RandBot(rand=self.__rand), my_move)
67+
else:
68+
# I am the leader bot
69+
me = leader_bot = FirstFixedMoveThenBaseBot(RandBot(rand=self.__rand), my_move)
70+
# We assume the other bot just random
71+
follower_bot = RandBot(self.__rand)
72+
73+
new_game_state, _ = engine.play_at_most_n_tricks(game_state=gamestate, new_leader=leader_bot, new_follower=follower_bot, n=self.__depth)
74+
75+
if new_game_state.leader.implementation is me:
76+
my_score = new_game_state.leader.score.direct_points
77+
opponent_score = new_game_state.follower.score.direct_points
78+
else:
79+
my_score = new_game_state.follower.score.direct_points
80+
opponent_score = new_game_state.leader.score.direct_points
81+
82+
heuristic = my_score / (my_score + opponent_score)
83+
return heuristic
84+
85+
86+
class FirstFixedMoveThenBaseBot(Bot):
87+
def __init__(self, base_bot: Bot, first_move: Move) -> None:
88+
self.first_move = first_move
89+
self.first_move_played = False
90+
self.base_bot = base_bot
91+
92+
def get_move(self, perspective: PlayerPerspective, leader_move: Optional[Move]) -> Move:
93+
if not self.first_move_played:
94+
self.first_move_played = True
95+
return self.first_move
96+
return self.base_bot.get_move(perspective=perspective, leader_move=leader_move)

src/schnapsen/game.py

+16
Original file line numberDiff line numberDiff line change
@@ -1003,6 +1003,22 @@ def make_assumption(self, leader_move: Optional[Move], rand: Random) -> GameStat
10031003
return full_state
10041004

10051005

1006+
def make_cheating_assumption(self, leader_move: Optional[Move], rand: Random) -> GameState:
1007+
full_state = self.__game_state.copy_with_other_bots(_DummyBot(), _DummyBot())
1008+
if self.get_phase() == GamePhase.TWO:
1009+
return full_state
1010+
1011+
new_talon: list[Card] = full_state.talon.get_cards()
1012+
# keep the trump in place:
1013+
old_trump = new_talon.pop(len(new_talon) - 1)
1014+
rand.shuffle(new_talon)
1015+
new_talon.append(old_trump)
1016+
full_state.talon = Talon(new_talon)
1017+
1018+
return full_state
1019+
1020+
1021+
10061022
class _DummyBot(Bot):
10071023
"""A bot used by PlayerPerspective.make_assumption to replace the real bots. This bot cannot play and will throw an Exception for everything"""
10081024

0 commit comments

Comments
 (0)