Ich hatte mal nebenbei, ohne grössere Planung, einen Haufen Funktionen runtergeschrieben, die dann zwei computergesteuerte Spieler Backgammon gegeneinander spielen lassen. Wobei die ”Strategie” beider Spieler einfach ist, einen der möglichen Züge zufällig auszuwählen.
Ich habe versucht mich mit dem zusammenfassen von Werten zu Verbundtypen, die ja eine Art Vorstufe zu Klassen sind, zurück zu halten, und habe nur auf unterster Ebene zwei Sätze von Werten jeweils zu einem `namedtuple()` zusammengefasst. Alternative wären normale Tupel gewesen, oder Wörterbücher. Bei normalen Tupeln wäre es IMHO schlechter zu lesen, und bei Wörterbüchern hat man, wie ich ja schon in einem vorherigen Beitrag geschrieben habe, eine andere Syntax als das was letztendlich bei einem OOP-Entwurf heraus käme.
Bei der Namensgebung ist `Point` vielleicht etwas verwirrend: So heissen im Englischen die Felder/”Zungen” auf dem Spielbrett.
Im Code sieht man ein mögliche Art das Spielfeld zu modellieren: Eine Liste (`track`) mit jeweils dem Spieler und der Anzahl der Steine die auf dem jeweiligen Spielfeld stehen. Wenn kein Stein auf dem Feld steht, ist der Spieler `None` (und die Anzahl 0).
Die aktuelle Frage, wie man nur die Steine benutzen kann, die regelkonform gezogen werden können, wird in der Funktion `iter_legal_moves()` behandelt. Die geht, weitere Funktionen benutzend, die legalen Züge durch und liefert diese als Tripel aus drei Werten: Der Augenzahl die für den Zug verwendet wird, und Start- und Endfeld in der Nummerierung der Spielregeln (1-24). Wobei es auch sein kann, das der Start `None` ist, das ist dann ein Zug eines geschlagenen Steins in das Spielfeld, oder das Endfeld kann als `None` angegeben sein, dann wird der Spielstein mit diesem Zug aus dem Spiel heraus gewürfelt.
Der nächste Schritt um das in Richtung objektorientiertem Programm umzubauen, ist recht einfach: Aus den beiden Verbundtypen Klassen machen und schauen welche Funktionalität/Funktionen als Methoden in deren Verantwortlichkeit fallen. Danach könnte man schauen welche Parameter oft zusammen durch die Gegend gereicht werden, und ob man die sinnvoll zu einem oder mehreren Klassen zusammenfassen kann. Und dann gibt es noch Werte von Grunddatentypen, die man in einer Klasse kapseln könnte, um dem Typ einen Namen und dazugehörige Operationen geben zu können. `track` wäre da ein heisser Kandidat.
Code: Alles auswählen
#!/usr/bin/env python3
from collections import namedtuple
from enum import Enum
from itertools import cycle, islice
import random
import time
TRACK_LENGTH = 24
HOME_LENGTH = 6
#:
#: Pairs of point number and count of checkers for the setup of the game.
#:
SETUP = [(1, 2), (12, 5), (17, 3), (19, 5)]
CHECKERS_PER_PLAYER = sum(count for _, count in SETUP)
Player = Enum('Player', 'BLACK WHITE')
PLAYER_TO_CHARACTER = {
None: ' ',
Player.BLACK: 'B',
Player.WHITE: 'W',
}
Point = namedtuple('Point', 'player, count')
Move = namedtuple('Move', 'die_value, start, target')
Win = Enum('Win', 'NONE NORMAL GAMMON BACKGAMMON')
def roll_dice():
result = [random.randint(1, 6), random.randint(1, 6)]
if result[0] == result[1]:
result *= 2
return result
def get_opponent(player):
if player == Player.WHITE:
return Player.BLACK
if player == Player.BLACK:
return Player.WHITE
raise ValueError(f'{player} has no opposing value')
def create_point(player=None, count=0):
if count < 0:
raise ValueError('count must be positive')
if count == 0 and player is not None:
raise ValueError('empty point cannot have a player')
if count != 0 and player is None:
raise ValueError('non empty point must have a player')
return Point(player, count)
def point_is_empty(point):
return point.player is None and point.count == 0
def point_has_blot(point, player):
return point.player == get_opponent(player) and point.count == 1
def is_possible_start(point, player):
return point.player == player and point.count > 0
def is_possible_target(point, player):
return (
point.player == player
or point_is_empty(point)
or point_has_blot(point, player)
)
def add_checkers_to_point(point, player, value):
count = point.count + value
if count < 0:
raise ValueError('amount of checkers must not become negative')
return create_point(None if count == 0 else player, count)
def create_track():
track = [create_point()] * TRACK_LENGTH
for player in Player:
for point_number, count in SETUP:
put_checkers(track, player, point_number, count)
return track
def validate_point_number(track, point_number):
if not 0 < point_number <= len(track):
raise ValueError('point_number out of range')
def reverse_point_number(track, point_number):
validate_point_number(track, point_number)
return len(track) - point_number + 1
def point_number_to_index(track, player, point_number):
validate_point_number(track, point_number)
if player == Player.BLACK:
return point_number - 1
if player == Player.WHITE:
return reverse_point_number(track, point_number) - 1
raise ValueError(f'illegal player {player}')
def get_point(track, player, point_number):
return track[point_number_to_index(track, player, point_number)]
def set_point(track, player, point_number, point):
track[point_number_to_index(track, player, point_number)] = point
def put_checkers(track, player, point_number, count):
point = get_point(track, player, point_number)
if not point_is_empty(point):
raise ValueError('point must be empty')
set_point(track, player, point_number, create_point(player, count))
def is_possible_target_in_track(track, player, point_number):
return is_possible_target(get_point(track, player, point_number), player)
def get_start_point(track, player, point_number):
point = get_point(track, player, point_number)
if not is_possible_start(point, player):
raise ValueError(f'{point} cannot be source for {player}')
return point
def get_target_point(track, player, point_number):
point = get_point(track, player, point_number)
if not is_possible_target(point, player):
raise ValueError(f'{point} already occupied by opponent')
return point
def iter_track(track, player, max_count=TRACK_LENGTH, filter_for=None):
if filter_for is None:
filter_for = player
if player == Player.WHITE:
track = reversed(track)
elif player != Player.BLACK:
raise ValueError(f'illegal player {player}')
return (
(point_number, point)
for point_number, point in enumerate(islice(track, max_count), 1)
if point.player == filter_for
)
def count_checkers_in_game(bar, track, player, point_count=TRACK_LENGTH):
return (
bar[player]
+ sum(
point.count for _, point in iter_track(track, player, point_count)
)
)
def count_checkers_not_in_home(bar, track, player):
return count_checkers_in_game(
bar, track, player, TRACK_LENGTH - HOME_LENGTH
)
def count_checkers_in_opponents_home(track, player):
return sum(
point.count
for _, point in iter_track(
track, get_opponent(player), HOME_LENGTH, player
)
)
def create_move(die_value, start, target):
if start is None and target is None:
raise ValueError('start and target are `None`')
return Move(die_value, start, target)
def create_bar_move(target_point_number):
return create_move(target_point_number, None, target_point_number)
def create_bear_off_move(die_value, start_point_number):
return create_move(die_value, start_point_number, None)
def is_bar_move(move):
return move.start is None and move.target is not None
def is_bear_off_move(move):
return move.start is not None and move.target is None
def is_track_move(move):
return move.start is not None and move.target is not None
def move_handle_blot(bar, player, target_point):
if point_has_blot(target_point, player):
bar[target_point.player] += 1
target_point = create_point()
return target_point
def remove_checker(track, player, point_number):
start_point = get_start_point(track, player, point_number)
start_point = add_checkers_to_point(start_point, player, -1)
set_point(track, player, point_number, start_point)
def add_checker(bar, track, player, point_number):
target_point = get_target_point(track, player, point_number)
target_point = move_handle_blot(bar, player, target_point)
target_point = add_checkers_to_point(target_point, player, 1)
set_point(track, player, point_number, target_point)
def do_track_move(move, bar, track, player):
if bar[player] != 0:
raise ValueError('checker from bar must be moved first')
remove_checker(track, player, move.start)
add_checker(bar, track, player, move.target)
def do_bar_move(move, bar, track, player):
if bar[player] == 0:
raise ValueError(f'no checkers on bar for {player}')
bar[player] -= 1
add_checker(bar, track, player, move.target)
def do_bear_off_move(move, bar, track, player):
if bar[player] != 0:
raise ValueError('checker from bar must be moved first')
remove_checker(track, player, move.start)
def do_move(move, bar, track, player):
if is_track_move(move):
do_track_move(move, bar, track, player)
elif is_bar_move(move):
do_bar_move(move, bar, track, player)
elif is_bear_off_move(move):
do_bear_off_move(move, bar, track, player)
else:
ValueError(f'unexpected move {move}')
def iter_legal_bar_moves(track, player, dice_values):
for die_value in dice_values:
if is_possible_target_in_track(track, player, die_value):
yield create_bar_move(die_value)
def iter_legal_track_moves(bar, track, player, dice_values):
all_in_home = count_checkers_not_in_home(bar, track, player) == 0
for die_value in dice_values:
for point_number, point in iter_track(track, player):
assert is_possible_start(point, player), point
target_point_number = point_number + die_value
if target_point_number > len(track):
move = create_bear_off_move(die_value, point_number)
else:
move = create_move(die_value, point_number, target_point_number)
if (
all_in_home and is_bear_off_move(move)
or (
is_track_move(move)
and is_possible_target_in_track(track, player, move.target)
)
):
yield move
def iter_legal_moves(bar, track, player, dice_values):
dice_values = set(dice_values)
if bar[player] > 0:
yield from iter_legal_bar_moves(track, player, dice_values)
else:
yield from iter_legal_track_moves(bar, track, player, dice_values)
def check_for_simple_win(bar, track, player):
return count_checkers_in_game(bar, track, player) == 0
def check_for_gammon(bar, track, player):
return (
count_checkers_in_game(bar, track, get_opponent(player))
== CHECKERS_PER_PLAYER
)
def check_for_backgammon(bar, track, player):
return (
bar[get_opponent(player)] > 0
or count_checkers_in_opponents_home(track, get_opponent(player)) > 0
)
def get_win_type(bar, track, player):
result = Win.NONE
for check_func, win_type in [
(check_for_simple_win, Win.NORMAL),
(check_for_gammon, Win.GAMMON),
(check_for_backgammon, Win.BACKGAMMON),
]:
if check_func(bar, track, player):
result = win_type
else:
break
return result
def point_to_string(point):
return (
PLAYER_TO_CHARACTER[point.player]
+ (format(point.count, '2d') if point.count else ' ')
)
def bar_to_string(bar):
return ', '.join(
'{}: {}'.format(PLAYER_TO_CHARACTER[player], bar[player])
for player in Player
)
def print_game_state(bar, track, player):
time.sleep(0.5)
print('\033[2J\033[1;1H', end='') # ANSI code sequence to clear the screen.
half_length = len(track) // 2
for track_half in track[:half_length], reversed(track[half_length:]):
print('|'.join(point_to_string(point) for point in track_half))
print('{} | Bar: {}\n'.format(player, bar_to_string(bar)))
def play_game():
bar = dict.fromkeys(Player, 0)
track = create_track()
players = cycle(Player)
if random.randint(0, 1) == 1:
next(players)
for player in players:
print_game_state(bar, track, player)
dice_values = roll_dice()
while dice_values:
legal_moves = list(
iter_legal_moves(bar, track, player, dice_values)
)
if not legal_moves:
break
move = random.choice(legal_moves)
do_move(move, bar, track, player)
dice_values.remove(move.die_value)
win_type = get_win_type(bar, track, player)
if win_type != Win.NONE:
return player, win_type
def main():
winner, win_type = play_game()
print('End', winner, win_type)
if __name__ == '__main__':
main()