25.6. Partia
Partia szachowa polega na naprzemiennym wykonywaniu ruchów przez figury białe i czarne. Pierwszy ruch wykonują białe. W pliku Game.scala zdefiniowana jest klasa Game reprezentująca stan partii szachowej.
Plik Game.scala: package chess import compat.Platform.EOL import Board._ import FigureMoves._ abstract sealed class Game { val color: Color
val hist: List[Game]
val board: Board
def updated(move: Move) = new OngoingGame(color.other, Board.updateBoard(board, move), this :: hist, move) def move(from: Field, to: Field, promotion: Option[Figure] = None) = { def isMatching(game: OngoingGame) = game.lastMove.from == from && game.lastMove.to == to && (game.lastMove match { case PromotionMove(_,_,prom) => Some(prom) == promotion case _ => promotion == None }) validGames.filter(isMatching).toList.headOption } def validGames = nextGames.filter{ g => !g.isOtherKingUnderCheck } def nextGames = board.iterator.
filter{ case (_, figure) => figure.figureColor == color }.
flatMap{ case (from, figure) =>
figure.figureType match { case Rook | Bishop | Queen | Knight | King =>
val fieldss = figureMoves(figure,from,false) val gamesAfterRegularMoves = (freeDestinations(fieldss) ++ captureDestinations(fieldss)). map(to => updated(RegularMove(from,to))) val gamesAfterCastlingMoves = if (figure.figureType == King) castling(3,1,4,2)++castling(7,8,6,7) else Seq() gamesAfterRegularMoves ++ gamesAfterCastlingMoves case Pawn =>
val regularAndPromotionMoves = (captureDestinations(figureMoves(figure,from,true))++ freeDestinations(figureMoves(figure,from,false))). flatMap(to => if (to.isLastRow(color)) Seq(Figure(Queen,color),Figure(Rook,color),Figure(Bishop,color),Figure(Knight,color)). map(figure => updated(PromotionMove(from,to,figure))) else Seq(updated(RegularMove(from,to)))) val enPassantMoves = freeDestinations(figureMoves(figure,from,true)). filter(isEnPassantCapture(from,_)).map(to => updated(EnPassantMove(from, to, Field(to.col,from.row)))) regularAndPromotionMoves ++ enPassantMoves case _ => Seq.empty } } def isOtherKingUnderCheck: Boolean = { def isKingOnBoard(g: Game) = g.board.values.exists(fig => fig == Figure(King,color.other)) !nextGames.forall(isKingOnBoard) } def freeDestinations(fieldss: Seq[Seq[Field]]) = fieldss.flatMap(fields => fields.takeWhile(isFieldEmpty)) def captureDestinations(fieldss: Seq[Seq[Field]]) = { def hasEnemyFigure(field: Field) = board.get(field).map(_.figureColor) == Some(color.other) fieldss.flatMap(_.dropWhile(isFieldEmpty).take(1).filter(hasEnemyFigure)) } def isFieldEmpty(field: Field): Boolean = !board.contains(field) def castling(kingTo: Int, rookFrom: Int, rookTo: Int, otherCol: Int) = { val row = color.firstRow if (board.get(Field(4,row)) == Some(Figure(King,color)) && board.get(Field(rookFrom,row)) == Some(Figure(Rook,color)) && board.get(Field(rookTo,row)) == None && board.get(Field(kingTo,row)) == None && board.get(Field(otherCol,row)) == None && hist.forall(_.board.get(Field(4,row)) == Some(Figure(King,color))) && hist.forall(_.board.get(Field(rookFrom,row)) == Some(Figure(Rook,color))) && !updated(RegularMove(Field(4,row),Field(rookTo,row))).isOtherKingUnderCheck) Seq(updated(CastlingMove(Field(4,row), Field(kingTo,row), Field(rookFrom,row), Field(rookTo,row)))) else Seq() } def isEnPassantCapture(from: Field, to: Field) = this match { case GameStart => false case g:OngoingGame => g.board.get(g.lastMove.to) == Some(Figure(Pawn,color.other)) && g.lastMove.to == Field(to.col, from.row) && g.lastMove.from == Field(to.col, from.row + 2*(to.row-from.row)) } def isKingUnderCheck: Boolean = new OngoingGame(color.other, board, this :: hist, RegularMove(Field(0,0),Field(0,0))).isOtherKingUnderCheck def isGameFinished: Boolean = nextGames.forall(_.isOtherKingUnderCheck) || Set[Set[Figure]](Set(Figure(King,White),Figure(King,Black)), Set(Figure(King,White),Figure(King,Black),Figure(Bishop,White)), Set(Figure(King,White),Figure(King,Black),Figure(Bishop,Black)), Set(Figure(King,White),Figure(King,Black),Figure(Knight,White)), Set(Figure(King,White),Figure(King,Black),Figure(Knight,Black)), Set(Figure(King,White),Figure(King,Black),Figure(Knight,White), Figure(Knight,White)), Set(Figure(King,White),Figure(King,Black),Figure(Knight,Black), Figure(Knight,Black))). contains(board.values.toSet) || !(board :: hist.map(_.board)). groupBy(identity).values.toSet.filter(_.size >= 3).isEmpty def winner: Option[Color] = if (isGameFinished && isKingUnderCheck) Some(color.other) else None def sameColorDestinations(color: Color, fieldss: Seq[Seq[Field]]) = { def hasSameColorFigure(field: Field) = board.get(field).map(_.figureColor) == Some(color) fieldss.flatMap(_.dropWhile(isFieldEmpty).take(1).filter(hasSameColorFigure)) } def possiblePromotions(from: Field, to: Field): Set[Figure] = validGames.map(_.lastMove). filter{ case PromotionMove(f,t,_) => f == from && t == to case _ => false }.map(_.asInstanceOf[PromotionMove].figure).toSet } object GameStart extends Game { override val color = White override val hist: List[Game] = Nil override val board = startingBoard override def toString = "White to begin:\n"+showBoard(board) } final class OngoingGame( override val color: Color, override val board: Board, override val hist: List[Game], val lastMove: Move ) extends Game { override def toString = "Last move: "+color.other+" "+ lastMove.from+" to "+lastMove.to+EOL+showBoard(board) }
Klasa Game jest abstrakcyjna i posiada trzy abstrakcyjne
składowe. Wartość color (wiersz
) reprezentuje kolor figur gracza,
na którego ruch jest w danym momencie kolej. Wartość hist (wiersz
) jest listą wartości typu Game, reprezentującą poprzednie stany
gry — odpowiadające sytuacjom po poprzednich ruchach. Wartość board
(wiersz
) reprezentuje aktualny stan planszy.
Obiekt GameStart rozszerza klasę Game i reprezentuje początkowy stan partii — przed wykonaniem pierwszego ruchu. W tym obiekcie wartość color ma przypisaną wartość White, co jest odzwierciedleniem faktu, że grę w szachy rozpoczyna ruch białych. Pole hist ma wartość będącą listą pustą, a wartość pola board reprezentuje początkowe rozmieszczenie figur na planszy. Metoda toString tworzy reprezentację tekstową stanu gry, która w przypadku obiektu GameStart wygląda następująco.
scala> import chess._ import chess._ scala> GameStart res0: chess.GameStart.type = White to begin: abcdefgh 8RNBQKBNR8 7PPPPPPPP7 6........6 5........5 4........4 3........3 2pppppppp2 1rnbqkbnr1 abcdefgh
Klasa OngoingGame reprezentuje stan trwającej, rozpoczętej partii szachów. Oprócz trzech wartości odziedziczonych po klasie Game, posiada czwartą wartość — lastMove — reprezentującą ostatni wykonany na planszy ruch. Metoda toString jest nadpisywana i korzysta z wartości lastMove oraz color do wypisania informacji na temat ostatniego ruchu. Klasa Game posiada metodę updated pozwalającą na dodanie ruchu szachowego do istniejącej gry i utworzenie nowej instancji klasy OngoingGame, reprezentującej partię z dodanym nowym ruchem. Poniższe wyrażenie tworzy stan gry odpowiadający przesunięciu w pierwszym ruchu białych piona z pola d2 na d4.
scala> GameStart.updated(RegularMove(Field(4,2),Field(4,4))) res1: chess.OngoingGame = Last move: White d2 to d4 abcdefgh 8RNBQKBNR8 7PPPPPPPP7 6........6 5........5 4...p....4 3........3 2ppp.pppp2 1rnbqkbnr1 abcdefgh
Metoda move z klasy Game również pozwala na dodanie ruchu szachowego do istniejącej gry i utworzenie nowej instancji klasy OngoingGame, ale ta metoda dodatkowo sprawdza, czy podany ruch jest prawidłowy. Jeśli tak jest, to zwraca obiekt klasy Some zawierający nowy stan gry (nowy obiekt typu OngoingGame), a jeśli nie, to zwraca obiekt None. Metoda ma trzy parametry: dwa określające pola, z którego i na które ma zostać przeniesiona figura oraz trzeci parametr, typu Option[Figure], mający domyślny argument None, który może być użyty w przypadku ruchu piona połączonego z promocją na inną figurę. Poniższy przykład pokazuje użycie metody move w przypadku prawidłowego ruchu, zgodnego z regułami szachowymi. Wynikiem metody jest obiekt Some zawierający nowy stan gry.
scala> GameStart.move(Field(1,2),Field(1,4),None) res2: Option[chess.OngoingGame] = Some(Last move: White a2 to a4 abcdefgh 8RNBQKBNR8 7PPPPPPPP7 6........6 5........5 4p.......4 3........3 2.ppppppp2 1rnbqkbnr1 abcdefgh)
Z kolei następny przykład pokazuje ruch nieprawidłowy, niezgodny z zasadami szachów. Wynikiem wywołania metody jest obiekt None.
scala> GameStart.move(Field(1,2),Field(1,5),None) res3: Option[chess.OngoingGame] = None
Metoda move działa w ten sposób, że sprawdza czy dany ruch znajduje się na liście wszystkich prawidłowych ruchów możliwych w danej sytuacji. Możliwe ruchy (w postaci obiektów typu Game reprezentujących sytuację po każdym z takich ruchów) zwraca metoda validGames. W przypadku pierwszego ruchu białych, prawidłowe możliwe ruchy to ruchy każdym z pionów o jedno lub dwa pola oraz po dwa możliwe ruchy każdym ze skoczków. Daje to razem liczbę dwudziestu możliwych ruchów.
scala> GameStart.validGames.size res4: Int = 20
Metoda validGames buduje rezultat na podstawie listy ruchów zbudowanych przez nextGames, czyli takich z których niektóre mogą nie być prawidłowe z tego powodu, że po ich wykonaniu król jest szachowany. Metoda validGames odrzuca takie ruchy wykorzystując metodę isOtherKingUnderCheck, która sprawdza czy król przeciwnika jest szachowany.
W wierszu
wykonywana jest iteracja po wszystkich figurach planszy, a
w wierszu
filtr pozostawia te, które mają kolor odpowiadający
kolorowi następnego ruchu. Następnie w wywołaniu metody flatMap z
wiersza
obliczane są możliwe sekwencje ruchów każdej z figur. Ruchy
figury zależą od jej rodzaju. Wieża, skoczek, hetman, goniec i król
traktowane są podobnie (wiersz
). W ich przypadku wywoływana jest
najpierw metoda figureMoves zwracająca sekwencje ruchów zgodnych z
zasadami ruchu danej figury, a następnie wołane są metody
gamesAfterRegularMoves oraz gamesAfterCastlingMoves, których
wyniki są łączone w jedną sekwencję. Pierwsza z nich tworzy, na
podstawie sekwencji ruchów zwróconych przez figureMoves, możliwe
stany gry, do których może doprowadzić zwykły ruch figurą w danej
sytuacji jaka jest na planszy (nie wszystkie ruchy zgodne z zasadami
ruchu figury są możliwe w sytuacji, gdy na planszy znajdują się inne
figury — inne figury znajdujące się na planszy mogą ograniczyć liczbę
ruchów danej figury). Metoda woła metody freeDestinations i
captureDestinations, które zwracają możliwe ruchy odpowiednio na
pola niezajęte oraz zajęte przez figury przeciwnika. Metoda
isFieldEmpty sprawdza, czy dane pole na planszy jest puste. Metoda
gamesAfterCastlingMoves tworzy możliwe stany gry, do których może
doprowadzić roszada. Istnieje potencjalnie możliwość wykonania jednej
z dwóch roszad, a każdą z możliwości obsługuje wywołanie metody
castling. Oczywiście w przypadku figur innych niż król, liczba
możliwości wynosi zero. Metoda castling sprawdza warunki
dopuszczalności roszady, a więc to, czy król i wieża stoją na swoich
pozycjach i czy stały na nich od początku gry, czy pola pomiędzy nimi
są puste i czy pole, przez które przechodzi król, nie jest atakowane
przez przeciwnika. W przypadku, gdy roszada jest dozwolona, metoda
castling zwraca jednoelementową sekwencję. W przeciwnym przypadku
zwraca sekwencję pustą.
Pion jest w wywołaniu metody flatMap traktowany inaczej, niż
pozostałe figury (wiersz
i kolejne). W przypadku piona dozwolone są
ruchy na pola wolne do przodu lub ruchy na pola zajęte po skosie w
bok, przy czym w obu przypadkach, jeśli pion przechodzi do pola
znajdującego się w ostatnim rzędzie, to zbiór dozwolonych ruchów
uwzględnia możliwe promocje piona na inne figury. Dodatkowo, dozwolone
są ruchy bicia piona przeciwnika w przelocie, polegające na przejściu
po skosie w bok i w przód na puste pole, przez które w poprzednim
ruchu przeszedł pion przeciwnika. Do sprawdzenia, czy zachodzi
sytuacja bicia w przelocie służy metoda isEnPassantCapture.
Klasa Game zawiera jeszcze kilka innych, nieopisanych dotąd metod. Metoda isKingUnderCheck używa metody isOtherKingUnderCheck do sprawdzenia, czy król koloru odpowiadającego bieżącemu ruchowi jest szachowany. Metoda isGameFinished sprawdza, czy gra jest zakończona. Następujące warunki zakończenia gry są obsługiwane:
- po każdym możliwym ruchu król jest szachowany,
- na planszy zostały jedynie oba króle,
- na planszy zostały króle oraz jeden goniec lub jeden skoczek,
- na planszy zostały króle oraz dwa skoczki tego samego koloru,
- powtórzyła się trzykrotnie taka sama pozycja.
Inne warunki zakończenia gry (wieczny szach, 50 posunięć bez bicia i ruchu piona, zaproponowanie remisu przez jednego z graczy i przyjęcie propozycji przez drugiego) nie są obsługiwane przez program.
Metoda winner zwraca obiekt opcji, zawierający kolor zwycięzcy w przypadku, gdy gra jest zakończona zwycięstwem jednego z graczy. Metoda sameColorDestinations zwraca pola zajęte przez figury tego samego koloru, na które dana figura mogłaby się natknąć wykonując ruch zgodnie z podanymi na wejściu potencjalnymi polami docelowymi. Metoda possiblePromotions zwraca zbiór figur, na jakie potencjalnie może być promowany pion przesuwany z jednego pola na drugie.
Plik Game.scala:
package chess
import compat.Platform.EOL
import Board._
import FigureMoves._
abstract sealed class Game {
val color: Color 