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:

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.

Język programowania Scala Wydanie 2. Copyright © Grzegorz Balcerek 2016

Licencja Creative Commons

Ten utwór jest dostępny na licencji Creative Commons Uznanie autorstwa-Na tych samych warunkach 4.0 Międzynarodowe.

Oracle and Java are registered trademarks of Oracle and/or its affiliates. Other names may be trademarks of their respective owners.