Spielfeld Objektorientiert darstellen

Wenn du dir nicht sicher bist, in welchem der anderen Foren du die Frage stellen sollst, dann bist du hier im Forum für allgemeine Fragen sicher richtig.
Mustii-i
User
Beiträge: 6
Registriert: Freitag 21. Dezember 2018, 23:14

Hallo,

ich Arbeite erst seit kurzem mit Python (generell neu in der Programmierung) und wollte wissen wie man z.B. ein Backgammon Spielfeld Objektorientiert darstellt da ich mir nicht genau vorstellen kann wie ich das Programmieren soll und die Steine darauf bewegen lassen soll. :?: :?: :?:
Sirius3
User
Beiträge: 17754
Registriert: Sonntag 21. Oktober 2012, 17:20

Der erste Schritt ist, sich zu überlegen, welche Daten man hat, also hier eine Liste mit Feldern und jedes Feld hat eine bestimmte Anzahl an Steinen einer Farbe. Dann überlegst Du Dir, welche Operationen auf diesem Feld möglich sind. Und dann schreibst Du für jede Operation eine Methode.
Benutzeravatar
__blackjack__
User
Beiträge: 13116
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

Das gesamte Spielfeld, welches die Felder zusammenfasst, wäre auch ein Objekt mit Methoden. Man muss da also überlegen was an Funktionalität wo am besten aufgehoben ist. Also beispielsweise wie ”schlau” die einzelnen Felder sein müssen, oder ob das ganz einfach nur Container für die Anzahl der Steine und die Farbe auf dem Feld erfasst.

Apropos Farbe: das `enum`-Modul wird IMHO viel zu selten verwendet. :-)
„All religions are the same: religion is basically guilt, with different holidays.” — Cathy Ladman
Benutzeravatar
snafu
User
Beiträge: 6741
Registriert: Donnerstag 21. Februar 2008, 17:31
Wohnort: Gelsenkirchen

Mustii-i hat geschrieben: Freitag 21. Dezember 2018, 23:31 ich Arbeite erst seit kurzem mit Python (generell neu in der Programmierung)
Dann ist Objektorientierung zu diesem Zeitpunkt vermutlich noch eine zu hohe Hürde für dich. Versuche doch erstmal, es nur mit Funktionen zu lösen. Das ist nicht so schön, weil du jeder Funktion so eine Art Kontext (Spieler, Spielbrett, aktueller Spielstand) übergeben müsstest, aber vielleicht wird dir so etwas klarer, welche Struktur du für dein Spiel brauchst.

Wenn das klappt, dann könntest du für den Kontext im nächsten Schritt ein Dict verwenden. Auf dessen Grundlage wiederum lässt sich später eine Context-Klasse basteln. Und die kann man dann nach und nach in eigene Klassen für Spieler, Spielfeld, usw aufbröseln bis man einen ordentlichen Entwurf hat. So entsteht die Objektorientierung quasi während des Programmierens.

Dieser Ansatz wäre eine Alternative zum vorherigen Planen aller Klassen. Ob das besser oder schlechter ist, hängt wohl von der persönlichen Herangehensweise ab. Ich komme damit ganz gut klar, aber man muss halt auch bereit sein, immer wieder viel Code umzuschreiben.
Benutzeravatar
__blackjack__
User
Beiträge: 13116
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

Statt eines `dict` würde ich für den Zwischenschritt mit Funktionen und Verbundwerten statt vieler Einzelwerte eher `collections.namedtuple` oder `types.SimpleNamespace` verwenden, je nach dem ob die Werte unveränderlich sein sollen oder nicht. Das sieht IMHO besser aus im Quelltext und man muss, wenn man welche davon später durch eigene Klassen ersetzt, nicht überall die Syntax für den Zugriff ändern.

Falls externe Bibliotheken erlaubt sind, dann natürlich `attr` statt `namedtuple` und `SimpleNamespace`, weil man dann die Typen nicht ersetzten muss, sondern später einfach noch Methoden hinzufügen kann.
„All religions are the same: religion is basically guilt, with different holidays.” — Cathy Ladman
Mustii-i
User
Beiträge: 6
Registriert: Freitag 21. Dezember 2018, 23:14

Vielen dank für die vielen und schnellen Antworten und hilfen erstmal :D

@snafu wie würden sie da am besten vor gehen wenn ich sie fragen darf, also ich weiß auch nicht wie ich die Felder so gestalten kann das ich dann auch steine drauf "bewegen" kann. Ich glaube ich wollte da etwas programmieren wo ich selber nicht mal richtig drauf klar komme. :/
Benutzeravatar
snafu
User
Beiträge: 6741
Registriert: Donnerstag 21. Februar 2008, 17:31
Wohnort: Gelsenkirchen

Mustii-i hat geschrieben: Sonntag 23. Dezember 2018, 08:59 @snafu wie würden sie da am besten vor gehen wenn ich sie fragen darf, also ich weiß auch nicht wie ich die Felder so gestalten kann das ich dann auch steine drauf "bewegen" kann. Ich glaube ich wollte da etwas programmieren wo ich selber nicht mal richtig drauf klar komme. :/
Denk dabei mal nicht so sehr an den Stein, sondern an das Feld auf dem Brett, das belegt werden soll. Keine Belegung wäre None auf dem Feld und belegt wäre ein Stein()-Objekt auf dem Feld. Für den Anfang kann es stattdessen auch mit True und False dargestellt werden. Ein Spielfeld wäre eine zweidimensionale Liste, d. h. in der Liste sind nochmals Listen. Für ein 9x9 Spielfeld könnte das so aussehen:

Code: Alles auswählen

# Leeres Spielfeld erzeugen
spielfeld = [[False] * 9] * 9

# Stein in zweite Reihe, drittes Feld setzen
spielfeld[1][2] = True
Bedenke, dass die "menschliche" Zählung dabei um eins verringert werden muss, da Python ab 0 zählt, d. h. die zweite Reihe ist entsprechend am Index 1. Von wo aus die Zählung beginnt, ist dir überlassen. Ich würde unten links nehmen wie bei einem typischen Koordinatensystem.

Achso, und im Forum sind wir alle per Du. Und ich lege auch keinen großen Wert auf das Sie...
Sirius3
User
Beiträge: 17754
Registriert: Sonntag 21. Oktober 2012, 17:20

@snafu: ich weiß nicht, bei welchem Spiel Du bist, aber bei Backgammon gibt es 24 linear angeordnete Felder, auf jedem Feld können beliebig viele Steine einer Farbe liegen, zudem sollte man sich noch die Anzahl der geschlagenen Steine merken.

@Mustii-i: die Belegung eines Feldes könnte man z.B. mit einer Zahl beschreiben, negative Zahl Anzahl an weißen Steinen, positive Zahl Anzahl der schwarzen Steine.
Benutzeravatar
snafu
User
Beiträge: 6741
Registriert: Donnerstag 21. Februar 2008, 17:31
Wohnort: Gelsenkirchen

Sirius3 hat geschrieben: Sonntag 23. Dezember 2018, 10:54 @snafu: ich weiß nicht, bei welchem Spiel Du bist, aber bei Backgammon gibt es 24 linear angeordnete Felder (...)
Mir war nicht bewusst, dass es um Backgammon geht. Ich war gedanklich ganz allgemein bei einer simpel gehaltenen Brettbelegung, wo wie gesagt nur belegt oder nicht belegt wichtig sind. Jetzt habe ich gesehen, dass im Eingangs-Post Backgammon im Nebensatz als Beispiel steht.

Daher mal direkt an den Threadersteller: Beziehen sich deine Fragen auf Backgammon oder steht noch nicht fest, welches Spiel es bei dir wird?
Mustii-i
User
Beiträge: 6
Registriert: Freitag 21. Dezember 2018, 23:14

Vielen dank für die blitzschnellen und hilfreichen Antworten. Ich versuch mal auf die Tipps einzugehen vielen dank :D
Benutzeravatar
__blackjack__
User
Beiträge: 13116
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@snafu: Mir ging es umgekehrt, ich hatte das kleine „z.B.“ völlig überlesen und dachte bis eben es geht definitiv um Backgammon. :-)

@Mustii-i: Geht es denn nun tatsächlich um Backgammon? An den Antworten bisher siehst Du vielleicht schon, dass das nicht ganz unwichtig ist, denn bei Backgammon würde man einzelne Felder anders modellieren, als beispielsweise bei Dame, wo man jeden einzelnen Spielstein als Wert hätte, im Gegensatz zu Backgammon, wo man die Anzahl der Steine pro Feld als Zahl modellieren würde.
„All religions are the same: religion is basically guilt, with different holidays.” — Cathy Ladman
Mustii-i
User
Beiträge: 6
Registriert: Freitag 21. Dezember 2018, 23:14

Da ich anfangs vorhatte es Objektorientiert zu machen, dachte ich, ich beziehe meine frage generell damit ich weiß wie es geht aber da ich es jetzt erstmal nur programmieren will ist die frage jetzt natürlich nur auf Backgammon bezogen :D
Mustii-i
User
Beiträge: 6
Registriert: Freitag 21. Dezember 2018, 23:14

Könntet ihr mir evtl. sagen wie ich das programmieren kann wenn ich jetzt z.B. will das das Programm würfelt (was ich schon habe) danach aber ich will das man nur die steine benutzen kann die man laut regel auch nur spielen darf und erst mit einem klick auf die Leertaste der nächste Wurf betätigt wird?
Sirius3
User
Beiträge: 17754
Registriert: Sonntag 21. Oktober 2012, 17:20

@Mustii-i: zur Beantwortung der Frage wäre der Code hilfreich, den Du bisher geschrieben hast. Du mußt das Spielfeld durchgehen und die Felder bestimmen, die gespielt werden dürfen.
Benutzeravatar
__blackjack__
User
Beiträge: 13116
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

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.

Aber hier erst mal die zusammengehackte Funktionensammlung:

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()
„All religions are the same: religion is basically guilt, with different holidays.” — Cathy Ladman
Mustii-i
User
Beiträge: 6
Registriert: Freitag 21. Dezember 2018, 23:14

Nochmals viel Dank das geht hier mit den antworten wirklich schnell was ich sonst nicht so kenne :D

__blackjack__ danke für deine ausfühliche hilfe ich schau mir den code selbst mal genau an VIELEN DANK :O :D
Benutzeravatar
__blackjack__
User
Beiträge: 13116
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

Der erste Teil der tief hängenden Früchte: Die beiden `namedtuple()` in Klassen umgewandelt:

Code: Alles auswählen

#!/usr/bin/env python3
from enum import Enum
from itertools import cycle, islice
import random
import time

from attr import attrib, attrs

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',
}

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')


@attrs(frozen=True)
class Point:
    player = attrib(default=None)
    count = attrib(default=0)

    def __str__(self):
        return (
            PLAYER_TO_CHARACTER[self.player]
            + (format(self.count, '2d') if self.count else '  ')
        )

    def __attrs_post_init__(self):
        if self.count < 0:
            raise ValueError('count must be positive')
        if self.count == 0 and self.player is not None:
            raise ValueError('empty point cannot have a player')
        if self.count != 0 and self.player is None:
            raise ValueError('non empty point must have a player')    

    def is_empty(self):
        return self.player is None and self.count == 0

    def has_blot(self, player):
        return self.player == get_opponent(player) and self.count == 1

    def is_possible_start(self, player):
        return self.player == player and self.count > 0

    def is_possible_target(self, player):
        return (
            self.player == player or self.is_empty() or self.has_blot(player)
        )

    def checkers_added(self, player, value):
        count = self.count + value
        if count < 0:
            raise ValueError('amount of checkers must not become negative')
        return Point(None if count == 0 else player, count)


def create_track():
    track = [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():
        raise ValueError('point must be empty')
    set_point(track, player, point_number, Point(player, count))


def is_possible_target_in_track(track, player, point_number):
    return get_point(track, player, point_number).is_possible_target(player)


def get_start_point(track, player, point_number):
    point = get_point(track, player, point_number)
    if not point.is_possible_start(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 point.is_possible_target(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
        )
    )


@attrs
class Move:
    die_value = attrib()    
    start = attrib()
    target = attrib()
    
    def __attrs_post_init__(self):
        if self.start is None and self.target is None:
            raise ValueError('start and target are `None`')

    def is_bar_move(self):
        return self.start is None and self.target is not None

    def is_bear_off_move(self):
        return self.start is not None and self.target is None

    def is_track_move(self):
        return self.start is not None and self.target is not None

    def _execute_track_move(self, bar, track, player):
        if bar[player] != 0:
            raise ValueError('checker from bar must be moved first')
        
        remove_checker(track, player, self.start)
        add_checker(bar, track, player, self.target)

    def _execute_bar_move(self, bar, track, player):
        if bar[player] == 0:
            raise ValueError(f'no checkers on bar for {player}')
        
        bar[player] -= 1
        add_checker(bar, track, player, self.target)

    def _execute_bear_off_move(self, bar, track, player):
        if bar[player] != 0:
            raise ValueError('checker from bar must be moved first')    

        remove_checker(track, player, self.start)

    def execute(self, bar, track, player):
        # 
        # TODO Find a more object oriented solution, replacing the
        #   ``if`` cascade by object behaviour.
        # 
        if self.is_track_move():
            self._execute_track_move(bar, track, player)
        elif self.is_bar_move():
            self._execute_bar_move(bar, track, player)
        elif self.is_bear_off_move():
            self._execute_bear_off_move(bar, track, player)
        else:
            ValueError(f'unexpected move {self}')

    @classmethod
    def new_bar_move(cls, target_point_number):
        return cls(target_point_number, None, target_point_number)
        
    @classmethod
    def new_bear_off_move(cls, die_value, start_point_number):
        return cls(die_value, start_point_number, None)


def move_handle_blot(bar, player, target_point):
    if target_point.has_blot(player):
        bar[target_point.player] += 1
        target_point = Point()
    
    return target_point


def remove_checker(track, player, point_number):
    start_point = get_start_point(track, player, point_number)
    start_point = start_point.checkers_added(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 = target_point.checkers_added(player, 1)
    set_point(track, player, point_number, target_point)


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 Move.new_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 point.is_possible_start(player), point
            target_point_number = point_number + die_value
            
            if target_point_number > len(track):
                move = Move.new_bear_off_move(die_value, point_number)
            else:
                move = Move(die_value, point_number, target_point_number)
            
            if (
                all_in_home and move.is_bear_off_move()
                or (
                    move.is_track_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 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(map(str, 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)
            move.execute(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()
„All religions are the same: religion is basically guilt, with different holidays.” — Cathy Ladman
Benutzeravatar
__blackjack__
User
Beiträge: 13116
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

Die Liste `track` und das Wörterbuch `bar` in eigene Datentypen gekapselt. Nun sind die einfachsten/offensichtlichsten Klassen umgesetzt. Nun kann man als nächsten Schritt schauen welche Argumente herumgereicht werden und ob/welche in in einer oder mehreren Klassen zusammengefasst werden können.

Code: Alles auswählen

#!/usr/bin/env python3
from enum import Enum
from itertools import cycle, islice
import random
import time

from attr import attrib, attrs

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',
}

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')


@attrs(frozen=True)
class Point:
    player = attrib(default=None)
    count = attrib(default=0)

    def __str__(self):
        return (
            PLAYER_TO_CHARACTER[self.player]
            + (format(self.count, '2d') if self.count else '  ')
        )

    def __attrs_post_init__(self):
        if self.count < 0:
            raise ValueError('count must be positive')
        if self.count == 0 and self.player is not None:
            raise ValueError('empty point cannot have a player')
        if self.count != 0 and self.player is None:
            raise ValueError('non empty point must have a player')    

    def is_empty(self):
        return self.player is None and self.count == 0

    def has_blot(self, player):
        return self.player == get_opponent(player) and self.count == 1

    def is_possible_start(self, player):
        return self.player == player and self.count > 0

    def is_possible_target(self, player):
        return (
            self.player == player or self.is_empty() or self.has_blot(player)
        )

    def checker_added(self, player):
        return Point(player, self.count + 1)
    
    def checker_removed(self, player):
        return Point(None if self.count == 1 else player, self.count - 1)
            

class Track:
    
    def __init__(self):
        self.points = [Point()] * TRACK_LENGTH
        for player in Player:
            for point_number, count in SETUP:
                self._put_checkers(player, point_number, count)

    def __len__(self):
        return len(self.points)
    
    def __iter__(self):
        return iter(self.points)
    
    def __reversed__(self):
        return reversed(self.points)

    def __str__(self):
        lines = list()
        half_length = len(self) // 2
        for track_half in [
            self.points[:half_length], reversed(self.points[half_length:])
        ]:
            lines.append('|'.join(map(str, track_half)))
        return '\n'.join(lines)

    def validate_point_number(self, point_number):
        if not 0 < point_number <= len(self):
            raise ValueError('point_number out of range')

    def reverse_point_number(self, point_number):
        self.validate_point_number(point_number)
        return len(self) - point_number + 1

    def _point_number_to_index(self, player, point_number):
        self.validate_point_number(point_number)
        
        if player == Player.BLACK:
            return point_number - 1
        if player == Player.WHITE:
            return self.reverse_point_number(point_number) - 1
        
        raise ValueError(f'illegal player {player}')

    def get_point(self, player, point_number):
        return self.points[self._point_number_to_index(player, point_number)]

    def set_point(self, player, point_number, point):
        self.points[self._point_number_to_index(player, point_number)] = point

    def _put_checkers(self, player, point_number, count):
        point = self.get_point(player, point_number)
        if not point.is_empty():
            raise ValueError('point must be empty')
        self.set_point(player, point_number, Point(player, count))

    def is_possible_target(self, player, point_number):
        return self.get_point(player, point_number).is_possible_target(player)

    def get_start_point(self, player, point_number):
        point = self.get_point(player, point_number)
        if not point.is_possible_start(player):
            raise ValueError(f'{point} cannot be source for {player}')
        return point

    def get_target_point(self, player, point_number):
        point = self.get_point(player, point_number)
        if not point.is_possible_target(player):
            raise ValueError(f'{point} already occupied by opponent')
        return point

    def iter_points(self, player, max_count=TRACK_LENGTH, filter_for=None):
        if filter_for is None:
            filter_for = player
        track = self
        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(self, player, point_count=TRACK_LENGTH):
        return sum(
            point.count for _, point in self.iter_points(player, point_count)
        )


def count_checkers_in_game(bar, track, player, point_count=TRACK_LENGTH):
    return (
        bar.get_checker_count(player)
        + track.count_checkers(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 track.iter_points(
            get_opponent(player), HOME_LENGTH, player
        )
    )


@attrs
class Move:
    die_value = attrib()    
    start = attrib()
    target = attrib()
    
    def __attrs_post_init__(self):
        if self.start is None and self.target is None:
            raise ValueError('start and target are `None`')

    def is_bar_move(self):
        return self.start is None and self.target is not None

    def is_bear_off_move(self):
        return self.start is not None and self.target is None

    def is_track_move(self):
        return self.start is not None and self.target is not None

    def _execute_track_move(self, bar, track, player):
        if bar.get_checker_count(player) != 0:
            raise ValueError('checker from bar must be moved first')
        
        remove_checker(track, player, self.start)
        add_checker(bar, track, player, self.target)

    def _execute_bar_move(self, bar, track, player):
        if bar.get_checker_count(player) == 0:
            raise ValueError(f'no checkers on bar for {player}')
        
        bar.remove_checker(player)
        add_checker(bar, track, player, self.target)

    def _execute_bear_off_move(self, bar, track, player):
        if bar.get_checker_count(player) != 0:
            raise ValueError('checker from bar must be moved first')    

        remove_checker(track, player, self.start)

    def execute(self, bar, track, player):
        # 
        # TODO Find a more object oriented solution, replacing the
        #   ``if`` cascade by object behaviour.
        # 
        if self.is_track_move():
            self._execute_track_move(bar, track, player)
        elif self.is_bar_move():
            self._execute_bar_move(bar, track, player)
        elif self.is_bear_off_move():
            self._execute_bear_off_move(bar, track, player)
        else:
            ValueError(f'unexpected move {self}')

    @classmethod
    def new_bar_move(cls, target_point_number):
        return cls(target_point_number, None, target_point_number)
        
    @classmethod
    def new_bear_off_move(cls, die_value, start_point_number):
        return cls(die_value, start_point_number, None)


class Bar:
    
    def __init__(self):
        self.player2checker_count = dict.fromkeys(Player, 0)

    def __str__(self):
        return ', '.join(
            '{}: {}'.format(
                PLAYER_TO_CHARACTER[player], self.get_checker_count(player)
            )
            for player in Player
        )
    
    def get_checker_count(self, player):
        return self.player2checker_count[player]
    
    def is_possible_start(self, player):
        return self.get_checker_count(player) > 0
    
    def add_checker(self, player):
        self.player2checker_count[player] += 1
    
    def remove_checker(self, player):
        if self.player2checker_count[player] <= 0:
            raise ValueError('number of checkers cannot be negative')
        self.player2checker_count[player] -= 1


def move_handle_blot(bar, player, target_point):
    if target_point.has_blot(player):
        bar.add_checker(target_point.player)
        target_point = Point()
    
    return target_point


def remove_checker(track, player, point_number):
    start_point = track.get_start_point(player, point_number)
    start_point = start_point.checker_removed(player)
    track.set_point(player, point_number, start_point)


def add_checker(bar, track, player, point_number):
    target_point = track.get_target_point(player, point_number)
    target_point = move_handle_blot(bar, player, target_point)
    target_point = target_point.checker_added(player)
    track.set_point(player, point_number, target_point)


def iter_legal_bar_moves(track, player, dice_values):
    for die_value in dice_values:
        if track.is_possible_target(player, die_value):
            yield Move.new_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 track.iter_points(player):
            assert point.is_possible_start(player), point
            target_point_number = point_number + die_value
            
            if target_point_number > len(track):
                move = Move.new_bear_off_move(die_value, point_number)
            else:
                move = Move(die_value, point_number, target_point_number)
            
            if (
                all_in_home and move.is_bear_off_move()
                or (
                    move.is_track_move()
                    and track.is_possible_target(player, move.target)
                )
            ):
                yield move


def iter_legal_moves(bar, track, player, dice_values):
    dice_values = set(dice_values)
    if bar.is_possible_start(player):
        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):
    opponent = get_opponent(player)
    return (
        bar.get_checker_count(opponent) > 0
        or count_checkers_in_opponents_home(track, opponent) > 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 print_game_state(bar, track, player):
    time.sleep(0.5)
    print('\033[2J\033[1;1H', end='')  # ANSI code sequence to clear the screen.
    print(track)
    print(f'{player} | Bar: {bar}\n')


def play_game():
    bar = Bar()
    track = 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)
            move.execute(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()
„All religions are the same: religion is basically guilt, with different holidays.” — Cathy Ladman
Atalanttore
User
Beiträge: 407
Registriert: Freitag 6. August 2010, 17:03

@__blackjack__: Dein Code sieht für mich ziemlich fortgeschritten aus. Einige der unbekannten Sachen konnte ich mit Google und Doku einigermaßen verstehen, aber anderes erschließt sich mir nicht.
  1. Welche Art von Zuweisung wird mit `attrib()` ohne und mit Argumenten `attrib(default=None)` bzw. `attrib(default=0)` durchgeführt?
  2. Warum benötigt man für die Zuweisung von `attrib()` den Dekorator `@attrs`?
Gruß
Atalanttore
Benutzeravatar
__blackjack__
User
Beiträge: 13116
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@Atalanttore: Ad 1.: Die Frage verstehe ich nicht so wirklich. Was meinst Du mit „Art von Zuweisung“? Wenn man keine Argumente an `attrib()` übergibt, dann hat die Funktion Defaultwerte die dann genommen werden. Wenn man Beispielsweise nichts für `default` übergibt, dann ist der Defaultwert `attr.NOTHING`, damit die `__init__()` später erkennen kann, dass nichts übergeben wurde und dann meckern kann das nichts übergeben wurde.

Ad 2.: Mit `attrib()` werden ja nur Attribute auf der Klasse erzeugt. Irgendwie muss man ja von da zu den erzeugten Methoden kommen. Und das macht der Klassendekorator. Der geht durch die Klassenattribute und erzeugt die ganzen Methoden auf der Klasse die dann dafür sorgen das es die Attribute auf den Objekten gibt und das die vergleich- und hashbar sind, eine nette `repr()` haben, und so weiter.

Man hätte das auch mit einer Basisklasse oder einer Metaklasse lösen können, aber als Dekorator ist es flexibler. Zum Beispiel wenn man es mit einer Klasse verwenden möchte, wo man schon ähnliche ”Magie” verwendet wie bei den meisten ORMs.
„All religions are the same: religion is basically guilt, with different holidays.” — Cathy Ladman
Antworten