Next: Class BOARD Up: Sather 1.0 Tutorial Previous: Class MOVE

Class POS

The main secret of class POS is the internal addressing scheme for a chess board. From outside, board positions are addressed in standard chess notation, e.g., the position in the lower left corner is called ``a1". Internally, POS numbers the positions row-wise from 0 to 63 which eases addressing computations. The correspondence is shown in the following tables:

External addressing scheme:

Internal addressing scheme:

POS is capable of returning all board positions which are reachable from an POS object's position by moves in various directions. The way! iter is used for this purpose. Possible directions are vertical, horizontal, diagonal, knight jumps and so on.

POS is declared to be a subtype of $IS_EQ{POS}. The $IS_EQ type is specified in the library file Library/abstract.sa. The essential meaning of this subtype declaration is that POS is required to offer a routine with the signature is_eq(x:SAME):BOOL. The existence of this routine is checked during compilation. The analogous situation holds for $STR. This abstract type requires the existence of a routine str:STR that prints out a reasonable string representation of the object.

In lines 2-4 is an example of a rather weird form of constant declaration. All together 12 integer constants are declared. The first one (knight) is assigned the value 1, the next one (diag_up_right) is set to 2 and so on. This form of constant declaration only works for integers and requires that there is no type identifier INT. Both


                knight:INT:=1, ...
and

                knight := 'a', ...
result in errors at compile time.

The internal address of a position is stored in the private attribute absolute declared in line 8.



class POS < $IS_EQ{POS}, $STR is
  
  const knight  := 1, diag_up_right, diag_up_left, diag_dn_right, diag_dn_left,
                      horizontal_right, horizontal_left, vertical_up, vertical_dn,
                      north_two, south_two, ring;

  -- The correct funtionality relies on the fact that diag_up_right to 
  -- vertical_dn are in that order. The implementation of $PIECE::move! may 
  -- depend on it.

  private attr absolute : INT;
  
  create:SAME is
    return new;
  end;

The following routines are used to handle ``internal" addresses of board positions.

  

  private pos(position:INT) is  -- write routine
    absolute := position;
  end;

  pos:INT is  -- reader routine
    return absolute;
  end;

  private row(p:POS):INT is
    return (p.pos/8);
  end;
  
  private column(p:POS):INT is
    return (p.pos%8);
  end;

The following routines represent the ``external" addressing scheme.

We discuss the routine check_pos first. The routine digit_value, which is implemented in the CHAR library class (see file Library/char.sa for details) returns the value of a character. For example '7'.digit_value=7. Note, that '\0't=0 and '7'.int /= 7.

The routine pos in line 39 is a good example for overloading. For dealing with the internal addressing scheme, there is already a routine called pos in line 12. That routine takes an INT as its parameter. In contrast: the following routine, accepts a STR parameter. The compiler determines, depending on the arguments which are present at a call, which of these routines has to be called.

Because of this mechanism, there cannot be two routines that have the same parameters and are different in their return types. If such a pair would be allowed, the compiler could not figure out for example which type an attribute with an implicit type declaration (e.g. A::routine) is meant to have.

Routine pos is the first occurrence of a pre condition in this tutorial, see line 40. The pre condition is a boolean expression that is checked on each call of the routine. If it is evaluated to true, the routine gets executed. Otherwise, it is a fatal error. Analogously, a post condition could have been declared. Note, that pre conditions are not checked by default. Checking can be invoked with the compiler flag -pre <classes> A frequent source of syntx error is that there may not be a semicolon behind a pre-condition because it is part of the header.

  
  check_pos(position:STR):BOOL is
    str : STR := position.lower;
    if str.size /= 2 then
      return false;
    end;
    row : INT := str.char(1).digit_value - 1;
    if row < 0 or row > 7 then
      return false;
    end;
    col : CHAR := str.char(0);
    if col < 'a' or col > 'h' then
      return false;
    end;
    return true;
  end; -- of check_pos

  pos(position:STR)  -- overloaded writer routine
  pre check_pos(position)
  is
    str : STR := position.lower;
    row : INT := str.char(1).digit_value - 1;
    col : CHAR := str.char(0);
    case col
      when 'a' then absolute := 0;
      when 'b' then absolute := 1;
      when 'c' then absolute := 2;
      when 'd' then absolute := 3;
      when 'e' then absolute := 4;
      when 'f' then absolute := 5;
      when 'g' then absolute := 6;
      when 'h' then absolute := 7;
    end;
    absolute := absolute + 8 * row;
  end; -- of pos(STR)

  str:STR is
    ret ::= #STR;
    ret := ret + column;
    ret := ret + row;
    return ret;
  end;

The routine row in line 63 is overloaded as well. The compiler can differentiate between row(INT):INT of line 18 and row:CHAR because of the different number of parameters.

In the statement in line 64 the result of the computation is an integer value. The library class INT offers two ways to convert integers into characters. The difference is best shown by means of an example. Consider the integer value 0. The conversion done by digit_char returns the character '0'. The other conversion is done by a routine called char which has the result that 0.char = '\0'.

The routine str(POS) is used internally to map an internal address, which might be different from self, to standard chess notation.

  
  
  row:CHAR is
    return ((absolute/8) + 1).digit_char;
  end;

  column:CHAR is
    col ::= absolute%8;
    case col
      when 0 then return 'a';
      when 1 then return 'b';
      when 2 then return 'c';
      when 3 then return 'd';
      when 4 then return 'e';
      when 5 then return 'f';
      when 6 then return 'g';
      when 7 then return 'h';
    end;
  end;

  private str(pos:INT):STR is
    ret ::= #STR;
    col ::= pos % 8;
    row ::= (pos / 8) + 1;
    case col
      when 0 then ret := "a";
      when 1 then ret := "b";
      when 2 then ret := "c";
      when 3 then ret := "d";
      when 4 then ret := "e";
      when 5 then ret := "f";
      when 6 then ret := "g";
      when 7 then ret := "h";
    end;
    
    ret := ret + row;
    return ret;
  end; -- of str(INT)

The following routines return neighboring addresses in standard chess notation. If there is no existing neighboring position for a given direction, the current address is returned.

    
    
  east:STR is
    ret ::= absolute + 1;
    if ret/8 /= absolute/8 then ret := absolute; end;
    return str(ret);
  end;

  west:STR is
    ret ::= absolute - 1;
    if ret/8 /= absolute/8 then ret := absolute; end;
    return str(ret);
  end;

  north:STR is
    ret ::= absolute + 8;
    if ret > 63 then ret := absolute; end;
    return str(ret);
  end;

  south:STR is
    ret ::= absolute - 8;
    if ret < 0 then ret := absolute; end;
    return str(ret);
  end;

In addition to routines that return the address of neighboring positions in horizontal and vertical directions, there are four routines for neighboring positions on the diagonal axes.


  
  northeast:STR is
    err : BOOL := false;
    ret ::= absolute + 8;
    if ret > 63 then ret := absolute; err := true; end;
    if ~err then
      ret := absolute + 1;
      if ret/8 /= absolute/8 then ret := absolute; err := true; end;
    end;
    if ~err then
      ret := absolute + 9;
    end;
    return str(ret);
  end;

  northwest:STR is
    err : BOOL := false;
    ret ::= absolute + 8;
    if ret > 63 then ret := absolute; err := true; end;
    if  ~err then
      ret := absolute - 1;
      if ret/8 /= absolute/8 then ret := absolute; err := true; end;
    end;
    if ~err then
      ret := absolute + 7;
    end;
    return str(ret);
  end;

  southeast:STR is
    err : BOOL := false;
    ret ::= absolute - 8;
    if ret < 0 then ret := absolute; err := true; end;
    if ~err then
      ret := absolute + 1;
      if ret/8 /= absolute/8 then ret := absolute; err := true; end;
    end;
    if ~err then
      ret := absolute - 7;
    end;
    return str(ret);
  end;

  southwest:STR is
    err : BOOL := false;
    ret ::= absolute - 8;
    if ret < 0 then ret := absolute; err := true; end;
    if ~err then
      ret := absolute - 1;
      if ret/8 /= absolute/8 then ret := absolute; err := true; end;
    end;
    if ~err then
      ret := absolute - 9;
    end;
    return str(ret);
  end;

Here are some equality tests. The first one is required because POS has been declared to be a subtype of $IS_EQ{POS}. The Sather compiler considers a boolean expression p=q to be syntactic sugar for the routine call p.is_eq(q). Analogously, p/=q is taken to be p.is_neq(q). If these expressions are found somewhere in the code, the corresponding routine has to be provided.


  is_eq(p:SAME):BOOL is
    return (absolute = p.pos);
  end;

  is_neq(p:STR):BOOL is
    return ~is_eq(p);
  end;
  
  is_eq(p:STR):BOOL is
    tmp ::= #POS;
    tmp.pos := p;
    return is_eq(tmp);
  end;

The iter way! returns all reachable positions on an otherwise empty board in the specified direction.

Since this is the first occurrence of an iter declaration, some explanations are appropriate. Iters are declared similar to routines. The difference is that their name has to end with an exclamation point ``!". Iters may only be called from within loop statements.

For each textual iter call, en execution state is maintained. When a loop is entered, the execution state of all iter calls is initialized. When an iter is called for the first time, the expressions for self and for each argument are evaluated.

When the iter is called, it executes the statements in its body in order. If it executes a yield statement, control and a value are returned to the caller. Subsequent calls to the iter resume execution with the statement following the yield statement. If an iter executes a quit statement or reaches the end of its body, control passes immediately to the end of the innermost enclosing loop statement in the caller and no value is returned from the iter.

The code in lines 180-183 is evaluated only at the time of the first invocation. If there are two different textual calls of way!, each one has a separate state and each will execute these code lines at the first invocation.

In line 182 the starting position of the stepping is initialized. Note that this assignment is actually a call of the private routine pos(INT). The compiler considers this expression to be equivalent to stepped.pos(absolute).

The loop in lines 184-348 is the main part of the iter. From inside the loop potential positions are returned to the caller. If no more positions are available, then a ``quit" ends this loop, ends the iter and ends the loop surrounding the call to the iter.

Since most branches of the case statement are similar only the first case (lines 186-198) is explained in some detail. Later we will point out the differences of the branches for knight, pawn, and king moves. From the current position which is kept in stepped, the northeast neighbor is checked. If this position is still on the board it is returned to the caller. This is done in line 192 by the yield statement.

After the caller has processed the new position, the next call to the iter will resume after line 192. The status is still available, i.e., stepped keeps the position, which has been returned previously. Since the only statement of the loop is this case, the iter will next re-execute the case and automatically re-enter this branch. (Note, the direction is not re-evaluated and remains unchanged.)

If the end of the board has been reached by moving into the northeast direction, the iter cannot return further valid position. Hence, the iter quits in the else branch (line 194 or 196). It does not return any position, and immediately terminates the loop in the caller.

  
  
  way!(direction:INT):POS is
    ret, stepped : POS;
    stepped := #POS;
    stepped.pos := absolute;  -- starting position
    count : INT := 0;
    
    loop
      case direction
      
      when diag_up_right then
        if stepped.column < 'h' then
          if stepped.row < '8' then
            stepped.pos := stepped.northeast;
            ret := #POS;
            ret.pos := stepped.pos;
            yield ret;
          else
            quit;
          end;
        else
          quit;
        end;
        
      when diag_up_left then
        if stepped.column > 'a' then
          if stepped.row > '1' then
            stepped.pos := stepped.southwest;
            ret := #POS;
            ret.pos := stepped.pos;
            yield ret;
          else
            quit;
          end;
        else
          quit;
        end;
        
      when diag_dn_right then
        if stepped.column < 'h' then
          if stepped.row > '1' then
            stepped.pos := stepped.southeast;
            ret := #POS;
            ret.pos := stepped.pos;
            yield ret;
          else
            quit;
          end;
        else
          quit;
        end;
        
      when diag_dn_left then
        if stepped.column > 'a' then
          if stepped.row < '8' then
            stepped.pos := stepped.northwest;
            ret := #POS;
            ret.pos := stepped.pos;
            yield ret;
          else
            quit;
          end;
        else
          quit;
        end;
        
      when vertical_up then
        if stepped.row < '8' then
          stepped.pos := stepped.north;
          ret := #POS;
          ret.pos := stepped.pos;
          yield ret;
        else
          quit;
        end;
        
      when vertical_dn then
        if stepped.row > '1' then
          stepped.pos := stepped.south;
          ret := #POS;
          ret.pos := stepped.pos;
          yield ret;
        else
          quit;
        end;
        
      when horizontal_right then
        if stepped.column < 'h' then
          stepped.pos := stepped.east;
          ret := #POS;
          ret.pos := stepped.pos;
          yield ret;
        else
          quit;
        end;
        
      when horizontal_left then
        if stepped.column > 'a' then
          stepped.pos := stepped.west;
          ret := #POS;
          ret.pos := stepped.pos;
          yield ret;
        else
          quit;
        end; -- way! will be continued ...

The branch of the case statement that computes the new position of a knight in lines 274-296 is somewhat different. Instead of using a current position (called stepped), the new positions are always computed relative to the starting position.

A white pawn (case north_two, lines 297-307) may move one or to steps to the north depending on the staring row. A black pawn (case south_two, lines 308-318) may move one or to steps to the south depending on the staring row. A king (case ring, lines 319-342) can reach all 8 positions on the ring around his staring position.

                
      when knight then
        
        ret := #POS;
        case count
          when 0 then ret.pos := absolute + 6;
          when 1 then ret.pos := absolute - 6;
          when 2 then ret.pos := absolute + 10;
          when 3 then ret.pos := absolute - 10;
          when 4 then ret.pos := absolute + 15;
          when 5 then ret.pos := absolute - 15;
          when 6 then ret.pos := absolute + 17;
          when 7 then ret.pos := absolute - 17;
        else
          quit;
        end;
        count := count + 1;
        if ret.pos <= 63 and ret.pos >= 0
           and column(ret) <= column(self) + 2
           and column(self) - 2 <= column(ret)
           and row(ret)    <= row(self) + 2
           and row(self) - 2    <= row(ret)
        then
          yield ret;
        end;
        
      when north_two then
      
        if count < 2 and stepped /= stepped.north then
          stepped.pos := stepped.north;
          ret := #POS;
          ret.pos := stepped.pos;
          count := count + 1;
          yield ret;
          if row /= '2' then quit; end;
        else
          quit;
        end;
        
      when south_two then
      
        if count < 2 and stepped /= stepped.south then
          stepped.pos := stepped.south;
          ret := #POS;
          ret.pos := stepped.pos;
          count := count + 1;
          yield ret;
          if row /= '7' then quit; end;
        else
          quit;
        end;
        
      when ring then
        
        ret := #POS;
        case count
          when 0 then ret.pos := north;
          when 1 then ret.pos := south;
          when 2 then ret.pos := east;
          when 3 then ret.pos := west;
          when 4 then ret.pos := northeast;
          when 5 then ret.pos := northwest;
          when 6 then ret.pos := southeast;
          when 7 then ret.pos := southwest;
        else
          quit;
        end;
        count := count + 1;
        if    ret.pos <= 63 and ret.pos >= 0
          and ret.pos /= absolute
           and column(ret) <= column(self) + 1
           and column(self) - 1 <= column(ret)
           and row(ret)    <= row(self) + 1
           and row(self) - 1    <= row(ret)
         then
          yield ret;
        end;
        
      else
        -- The else case was put in for reasons of 
        -- fail safe program development.
        raise "POS:way! invalid case\n";
      end; -- of case
    end; -- of loop
  end; -- of way!

end; -- of class POS



Next: Class BOARD Up: Sather 1.0 Tutorial Previous: Class MOVE


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