Open seblin opened 6 years ago
Hier mal eine frühe Version auf Basis des Vorschlags von @sebastian2443:
from collections import namedtuple
from random import choice
SQUARE_NAMES = ('inner', 'mid', 'outer')
MIN_INDEX = 0
MAX_INDEX = 7
SQUARE_INDICES = range(MIN_INDEX, MAX_INDEX + 1)
COLORS = ('white', 'black')
class Field(namedtuple('Field', 'square_name, index, owner')):
def __new__(cls, square_name, index, owner=None):
if square_name not in SQUARE_NAMES:
raise ValueError('Invalid square name')
if not MIN_INDEX <= index <= MAX_INDEX:
raise ValueError('Field index out of range')
return tuple.__new__(cls, (square_name, index, owner))
class Board(object):
def __init__(self):
self.fields = {
square: [Field(square, i) for i in SQUARE_INDICES]
for square in SQUARE_NAMES
}
def get_all_fields(self, player=None):
if not player:
return self.fields.copy()
return {
square: [f for f in self.fields[square] if f.owner == player]
for square in SQUARE_NAMES
}
def move_piece(self, player, old_field=None, new_field=None):
# Get original field to avoid cheating
new_field = self.fields[new_field.square_name][new_field.index]
if new_field.owner:
msg = 'New field already owned by {}'
raise ValueError(msg.format(new_field.owner.name))
new_field = new_field._replace(owner=player)
self.fields[new_field.square_name][new_field.index] = new_field
class Player(object):
def __init__(self, name, color):
if color not in COLORS:
raise ValueError('Invalid color')
self.name = name
self.color = color
def make_move(self, board):
# Use a random field for testing
square_name = choice(SQUARE_NAMES)
index = choice(SQUARE_INDICES)
field = Field(square_name, index, self)
board.move_piece(self, new_field=field)
def __repr__(self):
return '{}(name={!r}, color={!r})'.format(
type(self).__name__, self.name, self.color
)
board = Board()
walter = Player('Walter', 'white')
print(board.get_all_fields(walter))
walter.make_move(board)
print(board.get_all_fields(walter))
Wie gehen wir die Programmierung eigentlich an? TDD, d.h. erst einen Test schreiben und dann Code, der diesen Test besteht? Oder "klassisch" zuerst die jeweilige Methode erstellen und den Test mitliefern bzw zeitnah nachliefern?
Ich sehe zum ersten Mal eine Dict Comprehension. Wollen wir Code generell so kurz wie möglich halten?
Ich finde die frühe Version schon recht gut. Wir müssen mal schauen wie wir unsere Gottklasse 'Game' da einbringen. Wenn wir später 'Game' instanzieren, übergeben wir eine Referenz an die GUI, oder instanzieren wir das im init von der GUI?
Ich habe noch nie einen UNIT-Test geschrieben... Muss erst mal schauen wieviel für diesen Test geschrieben werden muss, bzw was ein Test überhaupt ausmacht.
Hier mal eine sehr frühe, aber funktionierende Version für unser is_mill() Problem: Unter dem Code kommt die Erklärung
class Point(object):
def __init__(self, x, y, z):
self.x = x
self.y = y
self.z = z
def __repr__(self):
return '\n|{}|\n|{}|\n|{}|'.format(
self.x, self.y, self.z
)
class Cube(object):
SQUARE_NAME = {'inner': 0, 'mid': 1, 'outer': 2}
INDEX = ([0,2],[1,2],[2,2],[2,1],[2,0],[1,0],[0,0],[0,1])
def __init__(self, dimension):
self.dimension = dimension
def convert_to_cube_coordinates(self, field):
point = Point( Cube.INDEX[field.index][0],
Cube.SQUARE_NAME[field.square_name],
Cube.INDEX[field.index][1]
)
return point
def convert_to_game_coordinates(self, point):
name = ''
index = -1
for axis in Cube.SQUARE_NAME:
if Cube.SQUARE_NAME[axis] == point.y:
name = axis
break
for nr, value in enumerate(Cube.INDEX):
if value[0] == point.x and value[1] == point.z:
index = nr
break
return Field(name, nr)
def determine_mill_rows(self, field):
rows = []
point = self.convert_to_cube_coordinates(field)
if self.is_corner(point):
row_first_axis = self.generate_row('x', point)
row_last_axis = self.generate_row('z', point)
rows = [row_first_axis, row_last_axis]
else:
row_first_axis = self.generate_row('y', point)
if 0 < point.x < self.dimension[0]-1:
row_last_axis = self.generate_row('x', point)
elif 0 < point.z < self.dimension[2]-1:
row_last_axis = self.generate_row('z', point)
rows = [row_first_axis, row_last_axis]
possible_mills = []
for row in rows:
fields = []
for point in row:
fields.append(self.convert_to_game_coordinates(point))
possible_mills.append(fields)
return possible_mills
def generate_row(self, axis, point):
points = []
if axis == 'y':
for y in range(0, self.dimension[1]):
points.append(Point(point.x, y, point.z))
elif axis == 'x':
for x in range(0, self.dimension[0]):
points.append(Point(x, point.y, point.z))
elif axis == 'z':
for z in range(0, self.dimension[2]):
points.append(Point(point.x, point.y, z))
return points
def is_corner(self, point):
if not 0 < point.x < self.dimension[0]-1 and not 0 < point.z < self.dimension[2]-1:
return True
return False
DIMENSION = (3,3,3)
walter = Player('Walter', 'white')
cube = Cube(DIMENSION)
field = Field('outer', 5, walter)
muehlen = cube.determine_mill_rows(field)
for reihe in muehlen:
print(reihe)
Erklärung: Der Spieler 'Walter' sucht sich ein Feld aus (Spielstein setzen/bewegen) und die Klasse board.move_piece() überprüft die Validität des Zuges. Ist alles Ok, dann wird geprüft ob eine Mühle entstanden ist. Hier setzt mein Code an. Ich habe eine neue Klasse Cube und Points erstellt. Ich stelle mir das Feld als 3-Dimensionales Objekt vor. So kann ich die Mühle-Reihen berechnen(determine_mill_rows). Ich wandele beispielsweise das Feld Fields('outer', 5, walter) in eine 3D Koordinate um (convert_to_cube_coordinates). (1|2|0) -> (x|y|z) Als nächstes prüfe ich diesen Punkt auf eine mögliche Ecke im Würfel. Für Ecken sind immer die X und Z Koordinaten wichtig. Sind diese MIN oder MAX Werte der Dimension(3,3,3) des Würfels, handelt es sich IMMER um eine Ecke. 0|0, 0|2, 2|2, 2|0
Bei einer Ecke brauchen wir nur die X und Z Koordinaten für die Mühlen, und bei keiner! Ecke werden einmal Y und je nach Punkt X oder Z Koordinaten genutzt für die Mühlen.
Wenn das geschehen ist, wandele ich jeden Punkt wieder in ein Field(owner=None) um. Für jede mögliche Mühle sammele ich die Felder in einer Liste. Und die Felder einer Mühle kann ich mit dem vom Board abgleichen -> 3 mal gleicher Owner BINGO!
# gesetzter / bewegter Spielstein auf:
Field('outer', 5, walter)
#erste moegliche Muehle
[Field(square_name='inner', index=5, owner=None),
Field(square_name='mid', index=5, owner=None),
Field(square_name='outer', index=5, owner=None)]
# zweite moegliche Muehle
[Field(square_name='outer', index=6, owner=None),
Field(square_name='outer', index=5, owner=None),
Field(square_name='outer', index=4, owner=None)]
Oder ist das zu kompliziert gedacht?
Ich finde das auf dem ersten Blick sehr kompliziert. Ich verstehe auch nicht, welchen Mehrwert mit den x,y,z-Koordinaten und dem ganzen Kram erreicht werden soll. Einen Vorschlag zu is_in_mill(), der deutlich kürzer ist, hatte ich schon in Issue #1 gezeigt. Vielleicht ist das untergegangen, daher hier nochmal:
SQUARE_NAMES = ('inner', 'mid', 'outer')
def is_in_mill(self, field):
square_fields = self.fields[field.square_name]
player = field.player
if (field.index % 2) == 0:
candidates = [
[square_fields[(field.index + i) % 8] for i in (0, 1, 2)],
[square_fields[(field.index - i) % 8] for i in (0, 1, 2)]
]
else:
candidates = [
[self.fields[square][field.index] for square in SQUARE_NAMES],
[square_fields[(field.index + i) % 8] for i in (-1, 0, 1)]
]
for candidate in candidates:
if all(field.player == player for field in candidate):
return True
return False
Sieht jemand Nachteile in diesem Vorgehen?
Oh, ich hatte deine Lösung gar nicht im Blick. Die ist schön kurz! Auf die Idee mit '% 8' wäre ich gar nicht gekommen. Ich habe also ne ganze Ecke zu kompliziert gedacht. Die Methode können wir so übernehmen. Eine Sache nur, wollen wir es jetzt 'field.owner' oder 'field.player' nennen?
Auf deiner Basis habe ich mal die Funktion get_allowed_fields geschrieben: Wenn ein Feld übergeben wird sucht es alle freien Nachbarfelder. Ohne Feld werden alle Felder ohne Besitzer zurückgegeben. So war es jaangedacht, oder?
def get_allowed_fields(self, field=None):
if field:
square_fields = self.fields[field.square_name]
square_index = SQUARE_NAMES.index(field.square_name)
player = field.player
candidates = [self.fields[SQUARE_NAMES[square_index-i]][field.index] for i in (-1,1) if 0 <= square_index-i <= 2] + \
[square_fields[(field.index + i) % 8] for i in (-1, 1)]
fields = { square: [] for square in SQUARE_NAMES }
[fields[field.square_name].append(field) for field in candidates if not field.player]
return { square: fields[square] for square in fields }
return {
square: [field for field in self.fields[square] if not field.player]
for square in SQUARE_NAMES
}
~~Ich weiß nicht, wie man aus der for-Schleife mit so vielen If Funktionen eine Comprehension schreibt. Glaube das wäre auch sonst zu undurchsichtig. Ist die Funktion 'Ok' für euch?~~ Habe es kürzer hinbekommen.
Für welchen Test entscheiden wir uns? Doctest oder Unittest? Ich habe etwas mit Unittest rumgespielt. Müsste das so für alle Funktionen ausgebaut werden?
import unittest
# root.py ; darunter liegt der derzeitige Code zum Muehle Spiel
import root
class TestBoard(unittest.TestCase):
def is_in_mill(self):
board = root.Board()
walter = root.Player('Walter', 'white')
board.fields['outer'][0] = root.Field(square_name='outer', index=0, player=walter)
board.fields['outer'][1] = root.Field(square_name='outer', index=0, player=walter)
board.fields['outer'][2] = root.Field(square_name='outer', index=0, player=walter)
field = root.Field('outer', 0, walter)
self.assertEqual(board.is_in_mill(field), True)
field = root.Field('outer', 1, walter)
self.assertEqual(board.is_in_mill(field), True)
field = root.Field('outer', 2, walter)
self.assertEqual(board.is_in_mill(field), True)
field = root.Field('mid', 1, walter)
self.assertEqual(board.is_in_mill(field), False)
if __name__ == '__main__':
test = TestBoard()
test.is_in_mill()
So, bei is_in_mill ist das ja wegen eines booleschen Wertes noch einfach. Aber wie läuft das jetzt bei den anderen Funktionen der Klasse Board? Je nach dem welche Felder von den Spielern besetzt sind, ist der Rückgabewerte von get_all_fields() ein anderer. Lösen wir das dann wie oben im Code? Einen festen Zustand hinterlegen und vergleichen dann das Ergebnis?
Generell: wäre es nicht eigentlich eine Idee, den Code nicht nur im Issue zu teilen, sondern einfach die ersten Dateien erstellt und bei Änderungen / Vorschlägen einen Merge-Request stellt? Ich verliere etwas die Übersicht von welcher Version wir denn nun ausgehen können usw.
Ich habe mal unseren Code zusammengefasst und in main.py gesichert. Der muss jetzt glaube nur noch freigegeben werden?
Welche Versionen meinst du? Wir haben bisher nur eine, die am Anfang von seblin. In den nachfolgenden Kommentaren haben wir nur zwei weitere Methoden erstellt, die man dann in den Code kopieren muss. Lässt sich trotzdem sehr viel besser über ein Merge-Request händeln.
Nach Abschluss von Issue #1 soll hier das Modell in Python-Code umgesetzt werden. Hierzu gehört auch das Abdecken mit Tests. Die Schnittstelle soll Wahrheitswerte, Listen und ähnliche Python-Objekte liefern. Ein Überführen in grafische Elemente mithilfe eines GUI-Frameworks erfolgt erst im nächsten Schritt.