Next: Type $PIECEand Related Up: Sather 1.0 Tutorial Previous: Class POS

Class BOARD

The two array whitexpieces and blackpieces store the pieces in the game. A piece is an object of type $PIECE which is explained below. Since both arrays are private, it is a secret of the board implementation in which way pieces are stored.

The board stores information about which color is to play (white_to_play) and about the last move (last_move). Moreover, the board knows whether the white or black king has been moved. This information is necessary, because castle moves are only allowed if the king has not been moved before.



class BOARD is

  private attr whitepieces : ARRAY{$PIECE};
  private attr blackpieces : ARRAY{$PIECE};

          attr white_to_play : BOOL;
          attr last_move : MOVE;
          attr white_K_moved : BOOL;
          attr black_K_moved : BOOL;

  create:SAME is
    ret::=new;
    ret.set_up;
    return ret;
  end;

The private routine set_up initializes the board. 16 white and 16 black pieces are placed onto the board, the first player is set to be white, both kings have not moved.


  private set_up is

    position ::= #POS;

    white_to_play := true;

    -- set up white pieces
    whitepieces := #(16);
    position.pos := "a2";
    loop i::=0.upto!(7);
      whitepieces[i] := #PAWN(position,PIECE::white);
      position.pos := position.east;
    end;
    position.pos := "a1"; whitepieces[8] := #ROOK(position,PIECE::white);
    position.pos := "b1"; whitepieces[9] := #KNIGHT(position,PIECE::white);
    position.pos := "c1"; whitepieces[10] := #BISHOP(position,PIECE::white);
    position.pos := "d1"; whitepieces[11] := #QUEEN(position,PIECE::white);
    position.pos := "e1"; whitepieces[12] := #KING(position,PIECE::white);
    position.pos := "f1"; whitepieces[13] := #BISHOP(position,PIECE::white);
    position.pos := "g1"; whitepieces[14] := #KNIGHT(position,PIECE::white);
    position.pos := "h1"; whitepieces[15] := #ROOK(position,PIECE::white);
    
    -- set up black pieces
    blackpieces := #(16);
    position.pos := "a7";
    loop i::=0.upto!(7);
      blackpieces[i] := #PAWN(position,PIECE::black);
      position.pos := position.east;
    end;
    position.pos := "a8"; blackpieces[8] := #ROOK(position,PIECE::black);
    position.pos := "b8"; blackpieces[9] := #KNIGHT(position,PIECE::black);
    position.pos := "c8"; blackpieces[10] := #BISHOP(position,PIECE::black);
    position.pos := "d8"; blackpieces[11] := #QUEEN(position,PIECE::black);
    position.pos := "e8"; blackpieces[12] := #KING(position,PIECE::black);
    position.pos := "f8"; blackpieces[13] := #BISHOP(position,PIECE::black);
    position.pos := "g8"; blackpieces[14] := #KNIGHT(position,PIECE::black);
    position.pos := "h8"; blackpieces[15] := #ROOK(position,PIECE::black);

    white_K_moved := false;
    black_K_moved := false;

    last_move := void;

    MAIN::display.redraw(self.str);
  end;

Several iters are needed to return all pieces on the board that fulfill a certain condition.

The first iter whitepiece! returns all white pieces, which are still alive. For this purpose, it makes use of the iter elt! in line 52. The iter is provided by the ARRAY library class (see file Library/array.sa). If elt! yields an element, this element is yield to the caller if it fulfills the conditions. However, if elt! quits, this loop is terminated as well, no element is returned to the caller.

  
  private whitepiece!:$PIECE is
    loop p ::= whitepieces.elt!;
      if ~void(p) and p.alive then yield p; end;
    end;
  end;

  private blackpiece!:$PIECE is
    loop
      p ::= blackpieces.elt!;
      if ~void(p) and p.alive then yield p; end;
    end;
  end;

The nesting depth of iters can be increased even further, as shown in my_piece below: Within whitepiece! the iter elt! is used. An element found by elt! is returned via whitepiece! and then returned to the caller of my_piece!. Similarly, a quit of elt!, induces a quit of whitepiece!, which in turn results in a quit of my_piece!. The latter terminates the loop, that must surround every call of my_piece! in the caller.

  
  my_piece!:$PIECE is
    if white_to_play then
      loop
        yield whitepiece!;
      end;
    else
      loop
        yield blackpiece!;
      end;
    end;
  end;

  private opp_piece!:$PIECE is
    if white_to_play then
      loop
        yield blackpiece!;
      end;
    else
      loop
        yield whitepiece!;
      end;
    end;
  end;

  piece!:$PIECE is
    loop
      yield whitepiece!;
    end;
    loop
      yield blackpiece!;
    end;
  end;

One of the secrets of the BOARD implementation is the way pieces are stored. For internal purposes it is necessary, to find out at which position of the arrays a particular piece is stored.

In the private routine index we use a post condition. To assure that the piece p is (dead or alive) on board we test whether the return value result is set appropriately, i.e., whether result is between 0 and 15 upon return. Note, that there may not be a semicolon behind a post condition. The conditions get checked before the routine returns. To access the value that will be returned, Sather provides the predefined results expression. The type of results is determined by the result type of the routine. If checking is desired, it has to be activated with the compiler flag -post <classes>.

The loop (line 97-104) is an excellent example of a loop that is controlled by multiple iters. The first two iters are defined in the ARRAY library class. The iter ind! (line 98) returns the existing indexes of array. As explained above, elt! (line 99) returns the corresponding array elements. For each iteration of the loop the following condition holds: whitepieces[i] = q. Both iters can be expected to yield the same number of times. If the end of the array is encountered, the call to ind! will terminate the loop; elt! will not be called.

However, if the desired piece is found, it is not necessary, to continue the search. To terminate the loop immediately, the predefined iter break! is called in line 102, which will always execute a quit statement.

The same search is implemented differently in the else branch (line 106). Here we use the library routine index_of provided in the ARRAY class. (See file Library/array.sa for details.)

  
  private index(p:$PIECE):INT 
  post result.is_bet(0,15)
  is  
    ret : INT := -1;
    if p.iswhite then
    
      loop
        
        i::= whitepieces.ind!;
        q::= whitepieces.elt!;
        
        if p.position = q.position then 
          ret := i; 
          
          break!; 
          
        end;
      end; -- of loop
      
    else
      
      ret := blackpieces.index_of(p);
      
    end;
    return ret;
  end; -- private index

To check whether there is a piece on a given position of the board the following routines are provided:


  has_piece(pos:POS):BOOL is
    ret : BOOL := false;
    loop p::=piece!;
      if p.position = pos then ret := true; end;
    end;
    return ret;
  end;

  has_white_piece(pos:POS):BOOL is
    ret : BOOL := false;
    loop p::=whitepiece!;
      if p.position = pos then ret := true; end;
    end;
    return ret;
  end;

  has_black_piece(pos:POS):BOOL is
    ret : BOOL := false;
    loop p::=blackpiece!;
      if p.position = pos then ret := true; end;
    end;
    return ret;
  end;

  has_my_piece(pos:POS):BOOL is
    if white_to_play then
      return has_white_piece(pos);
    else 
      return has_black_piece(pos);
    end;
  end;

The following two routines return a pointer to a piece at a given position of the board. The routine comes in two versions. The latter can process POS arguments by reducing them to STR parameters which are then processed by the first version.

  
  piece(str:STR):$PIECE is
    ret : $PIECE;
    position ::= #POS;
    position.pos := str;
    loop p::=piece!;
      if p.position = position then ret := p; end;
    end;
    return ret;
  end;

  piece(p:POS):$PIECE is
    return piece(p.str);
  end;

For interface purposes, a board can represent the status of all pieces in an ASCII representation. The character array is used to transmit the board situation to the ASCII_DISPLAY and via the X_DISPLAY to the external class XCW.

  

  str:ARRAY{CHAR} is
    ret::=#ARRAY{CHAR}(65);
    loop
      ret[0.upto!(63)] := ' ';
    end;
    ret[64] := '\0';
    loop p::=self.whitepiece!;
      if ~void(p) and p.alive then
        ret[p.position.pos] := p.fig;
      end;
    end;
    loop p::=self.blackpiece!;
      if ~void(p) and p.alive then
        ret[p.position.pos] := p.fig.lower;
      end;
    end;
    return ret;
  end;

After these helper routines and iters have been implemented, the central routines are presented. The routine pos_in_check tests whether a given position could be reached in the next move by the opponent.

In this routine there is again a good example of nested iter calls: The first loop (line 172-179) considers all pieces of the opponent player. The inner loop (line 173-178) then for each of these pieces considers target positions of potential moves. (Is is explained later on, what a move is if the flag for_check_test is set. Just ignore the flag for the time being.)

The call piece.move!() in line 174is a dispatched iter. See page for an alternative implementation that works with earlier releases of the Sather 1.0 compiler.


     
  pos_in_check(p:POS):BOOL is
    ret : BOOL;
    pos : POS;
    ret := false;
    
    loop piece::=opp_piece!;
      loop
        pos :=piece.move!(self, PIECE::for_check_test);
        if p=pos then
          ret := true;
          break!;
        end;
      end;
      if ret then break!; end;
    end;
    return ret;
  end; -- of pos_in_check

The routine my_king_isin_check returns true if the king of the current color (white_to_play) is in check. After an otherwise valid move of a piece, the own king is not allowed to be exposed and to be in check.

  
  my_king_isin_check:BOOL is
    piece : $PIECE;
    loop
      piece := my_piece!;
      until!(piece.isking);
    end;
    return pos_in_check(piece.position);
  end; -- of my_king_isin_check

Boolean expressions are evaluated with a short-circuit semantics. For an and this means that the second operand is only evaluated if the first operand was true. For an or the second operand is evaluated only if the first one was false. In routine check_n_apply_move we make use of this to ensure that a move is applied to a board only if it is valid.

Routine move_valid_so_far checks whether a given move is valid with respect to the current state of the board. The only circumstance which is not checked is whether the move would expose the own king to be in check.

  
  check_n_apply_move(move:MOVE):BOOL is
    return (move_valid_so_far(move) and apply_move_with_own_check_test(move));
  end; -- of check_n_apply_move
  
  
  private move_valid_so_far(move:MOVE):BOOL
  pre ~move.isquit
  is
    ret : BOOL := false;
    
    -- A valid move must start at a position where one of my pieces is....
    
    if has_my_piece(move.from) then
      p::=piece(move.from);
      
      -- ... and it must be a valid move with respect to the mobility of the
      -- piece at the current state of the board.
      
      if p.valid_move(move.to,self) then
        ret := true;
        
        -- Since the move seems to be valid, the moving piece is stored
        -- in the move object. That eases future access to the moving piece
        -- and allows for un-doing of moves.
        
        move.piece := p;
        
      end;
    end;
    return ret;
  end; -- of move_valif

The move is applied to the board in routine apply_move_with_own_check_test. The routine returns false, and leaves the state of the board unchanged, if an otherwise valid move would expose the own king to be in check.

First of all in lines 221-241 it is checked whether the move would kill an opponent piece. The normal circumstances for this are that the moving piece moves to a position that is occupied by an opponent piece. Chess has one special rule due to which a piece can be killed without moving to its former position. It is called an ``en passant" move. This special case can only occur if two pawns are involved. My pawn can kill an opponent pawn that sits immediately east or west of my pawn, if the other pawn has done an initial double move in the immediately preceding move. (That's why the last move is considered to be part of the state of a board.). If these conditions hold, my pawn can move diagonal so that his new position is ``behind" the opponent pawn.

Special action is required in case of castle moves. A castle move works as follows. If the king and a rook both are in their initial positions, if there is no piece in between them, if the king has not been moved in the game, and if the two positions next to the king in the direction toward the rook are not in check, then the king moves two positions towards the rook and then the rook jumps over the king and is put immediately next to the king. A castle move is a k-castle, if the king moves to the rook whose initial position is closer. Otherwise it is called q-castle, because due to the initial queen position, the distance to the rook is larger. Chess only allows castle moves, if the king has not been moved earlier in the game. The board keeps track of king moves in the two flags white_K_moved and black_K_moved. To enable un-doing of moves, a move knows whether it causes a change of a K_moved flag. See lines 254-268 for the K_moved flags and lines 269-286 for the implementation of castle moves.

Another special rule in chess allows to exchange a pawn against a queen or a knight when it reaches the base line of the opponent. Theoretically, a player could have 9 queens. This rule is implemented in lines 254-268.

  
  apply_move_with_own_check_test(move:MOVE):BOOL
  pre ~move.isquit and move_valid_so_far(move) and ~void(move.piece)
  is

    ret : BOOL := true; -- Will be false if the move is invalid due to
                        -- exposure of own "king in chess"

    p:$PIECE:=move.piece;  -- to be moved
    r:$PIECE;              -- may be killed


    -- Case 1: Kill with normal move
    r := piece(move.to);   -- If it exists, it can only be opponent piece.
                           -- Otherwise the move would not be valid.

    -- Case 2: En Passant. 

    if      void(r) and ~void(last_move) and p.ispawn
       and ~void(last_move.piece) and  last_move.piece.ispawn
       and (   last_move.to = p.position.east
            or last_move.to = p.position.west)
    then
      if    (     p.iswhite and  white_to_play
             and p.position.row = '5' and last_move.from.row = '7')
         or (    ~p.iswhite and ~white_to_play
             and p.position.row = '4' and last_move.from.row = '2')
      then
        r := last_move.piece;
      end;
    end;

    if ~void(r) then
      move.kills := r;
      r.alive := false;
    end;

    p.update_position(move.to);

    -- Deal with king moves. 
    
    if p.isking then
      if white_to_play and ~white_K_moved then
        white_K_moved := true;
        move.king_chg := true;
      end;
      if ~white_to_play and ~black_K_moved then
        black_K_moved := true;
        move.king_chg := true;
      end;
    end;

    -- Deal with pawn exchange. 
    
    if    (p.ispawn and  p.iswhite and  white_to_play and p.position.row='8')
       or (p.ispawn and ~p.iswhite and ~white_to_play and p.position.row='1')
    then
      case MAIN::player.ask_pawn_xchg
      when 'Q' then
        whitepieces[index(p)] := #QUEEN(p.position,PIECE::white);
      when 'K' then
        whitepieces[index(p)] := #KNIGHT(p.position,PIECE::white);
      else
        -- Do it fails safe.
        raise "BOARD:apply_move:pawn_exchange invalid case entry\n"
      end;
      move.pawn_chg := true;
    end;

    -- Deal with castles. 
    
    if move.isq_castle then
      if white_to_play then
        rook ::= piece("a1");
        rook.update_position("d1");
      else
        rook ::= piece("a8");
        rook.update_position("d8");
      end;
    elsif move.isk_castle then
      if white_to_play then
        rook ::= piece("h1");
        rook.update_position("f1");
      else
        rook ::= piece("h8");
        rook.update_position("f8");
      end;
    end;
    
    move.prev_move := last_move;
    last_move := move;

    -- Check whether my king is in check after application of the move
    
    if my_king_isin_check then
    
      -- Although otherwise correct this is an invalid move.
      -- The original state of the board is reconstructed by calling 
      -- unapply_move.
      
      ret := false;
      unapply_move;
    end;

    white_to_play := ~white_to_play;
    return ret;
    
  end; -- of apply_move

The routine unapply_move uses the information that is stored in last_move to replay the move, i.e., restore the board to the state it had before the application of that move. It depends on the fact that last_move is a valid move except that the king might be in check afterwards.


  unapply_move  is

    -- Restore killed opponent piece
    if ~void(last_move.kills) then
      last_move.kills.alive := true;
    end;

    -- Restore pawn exchange
    if last_move.pawn_chg then
      newpiece ::= piece(last_move.piece.position);
      whitepieces[index(newpiece)] := last_move.piece;
    end;

    -- Restore move
    last_move.piece.update_position(last_move.from);
    if last_move.king_chg then
      if white_to_play then
        white_K_moved := false;
      else
        black_K_moved := false;
      end;
    end;

    -- Restore castle
    if last_move.isq_castle then
      if white_to_play then
        rook ::= piece("d1");
        rook.update_position("a1");
      else
        rook ::= piece("d8");
        rook.update_position("a8");
      end;
    elsif last_move.isk_castle then
      if white_to_play then
        rook ::= piece("f1");
        rook.update_position("h1");
      else
        rook ::= piece("f8");
        rook.update_position("h8");
      end;
    end;

    last_move := last_move.prev_move;
    white_to_play := ~white_to_play;
  end; -- of unapply_move

For the automatic player, there must be a way to assign a worth to a board. This is done as follows. Compute the sum of the worths of all white pieces on the board. Similar, compute the worth of all black pieces. The value of the board is the ratio of the two values.

The routine board_value returns a floating point value, FLT, which is specified in the FLT library. (See file Library/flt.sa for details.)

More complex evaluation functions are known and can be used to replace the simple function board_value. For example, the degree of freedom the pieces have in their movement is an interesting aspect that might be considered in the evaluation function.

  
  board_value:FLT is
    white_value : INT := 0;
    black_value : INT := 0;
    loop p::= whitepiece!;
      white_value := white_value + p.worth;
    end;
    loop p::= blackpiece!;
      black_value := black_value + p.worth;
    end;
    return white_value.flt/black_value.flt;
  end; -- of board_value

end; -- of class BOARD



Next: Type $PIECEand Related Up: Sather 1.0 Tutorial Previous: Class POS


gomes@ICSI.Berkeley.EDU
Tue May 30 21:15:13 PDT 1995