quoridor/src/main/java/cz/xelfi/quoridor/Board.java
author Jaroslav Tulach <jaroslav.tulach@apidesign.org>
Sun, 30 Aug 2009 14:37:47 +0200
changeset 48 69e897fe8140
parent 40 e45bc8ad2eaf
child 75 6802034b7a6f
permissions -rw-r--r--
Spliting Game into GameId and Game with full info about the state of the game
     1 /*
     2  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
     3  *
     4  * The contents of this file are subject to the terms of either the GNU
     5  * General Public License Version 2 only ("GPL") or the Common
     6  * Development and Distribution License("CDDL") (collectively, the
     7  * "License"). You may not use this file except in compliance with the
     8  * License. You can obtain a copy of the License at
     9  * http://www.netbeans.org/cddl-gplv2.html
    10  * or nbbuild/licenses/CDDL-GPL-2-CP. See the License for the
    11  * specific language governing permissions and limitations under the
    12  * License.  When distributing the software, include this License Header
    13  * Notice in each file and include the License file at
    14  * nbbuild/licenses/CDDL-GPL-2-CP.  Sun designates this
    15  * particular file as subject to the "Classpath" exception as provided
    16  * by Sun in the GPL Version 2 section of the License file that
    17  * accompanied this code. If applicable, add the following below the
    18  * License Header, with the fields enclosed by brackets [] replaced by
    19  * your own identifying information:
    20  * "Portions Copyrighted [year] [name of copyright owner]"
    21  *
    22  * Contributor(s):
    23  *
    24  * Portions Copyrighted 2009 Jaroslav Tulach
    25  */
    26 
    27 package cz.xelfi.quoridor;
    28 
    29 import cz.xelfi.quoridor.Player.Direction;
    30 import java.io.BufferedReader;
    31 import java.io.EOFException;
    32 import java.io.IOException;
    33 import java.io.Reader;
    34 import java.io.StringReader;
    35 import java.io.StringWriter;
    36 import java.io.Writer;
    37 import java.util.ArrayList;
    38 import java.util.Arrays;
    39 import java.util.BitSet;
    40 import java.util.Collection;
    41 import java.util.Collections;
    42 import java.util.HashSet;
    43 import java.util.List;
    44 import java.util.Set;
    45 import java.util.regex.Matcher;
    46 import java.util.regex.Pattern;
    47 
    48 /**
    49  * Represents a snapshot of the game position,
    50  * including all the placed and not-yet placed fences and player positions.
    51  * It it can print itself to stream in
    52  * <a href="http://www.gamerz.net/pbmserv/quoridor.html">ascii art format<a/>
    53  * and can it read back. The class is immutable
    54  * but it contains {@link #apply(cz.xelfi.quoridor.Move)}
    55  * that produce new {@link Board} with position created after
    56  * applying some {@link Move}. Use:
    57  * <pre>
    58  * Board whiteOnTurn = Board.empty();
    59  * Board blackOnTurn = whiteOnTurn.apply(Move.NORTH);
    60  * Board whiteAgain = blackOnTurn.apply(Move.SOUTH);
    61  * Board withOneFence = whiteAgain.apply(Move.fence('D', 7));
    62  * </pre>
    63  * 
    64  * @author Jaroslav Tulach
    65  */
    66 public final class Board {
    67     /** players */
    68     private final List<Player> players;
    69     /** fences placed on board */
    70     private final Set<Fence> fences;
    71     /** occurpied bits (coordinates encoded by toIndex methods) 
    72                          [N]
    73                
    74         +-----------------------------------+
    75      6  |                                   |          
    76      5  |   +   +   +   +   +   +   +   +   |  
    77      4  |                                   |          
    78      3  |   +   +   +   +   +   +   +   +   |  
    79      2  |                                   |          
    80      1  |   +   +   +   +   +   +   +   +   |  
    81      0  |                                   |
    82      9  |   +   +   +   +   +   +   +   +   |
    83 [W]  8  |                                   |     [E]
    84      7  |   +   +   +   +   +   +   +   +   |
    85      6  |                                   |
    86      5  |   +   +   +   +   +   +   +   +   |
    87      4  |                                   |
    88      3  |   +   +   +   +   +   +   +   +   |
    89      2  |                                   |
    90      1  |   +   +   +   +   +   +   +   +   |
    91      0  |                                   |
    92         +-----------------------------------+
    93           0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6
    94                          [S]
    95      
    96      * even indexes == position of the pawns
    97      * odd indexes  == centers of fences
    98      * even x odd   == side of a fence
    99      * odd x even   == side of a fence
   100      */
   101     private final BitSet occupied;
   102     /** which player's turn is next? */
   103     private final int turn;
   104     
   105     /**
   106      * Creates a new instance of Board 
   107      */
   108     private Board (int x1, int y1, int f1, int x2, int y2, int f2) {
   109         this.players = Collections.unmodifiableList (Arrays.asList (new Player[] {
   110             new Player (x1, y1, f1, Player.Direction.NORTH),
   111             new Player (x2, y2, f2, Player.Direction.SOUTH),
   112         }));
   113         this.fences = Collections.emptySet ();
   114         try {
   115             this.occupied = computeOccupied (players, fences);
   116         } catch (IllegalPositionException ex) {
   117             throw new IllegalStateException (ex.getMessage ());
   118         }
   119         this.turn = 0;
   120     }
   121     
   122     /** Copy constructor that provides players and fences.
   123      */
   124     private Board (int turn, List<Player> players, Set<Fence> fences)
   125     throws IllegalPositionException {
   126         this.players = Collections.unmodifiableList (players);
   127         this.fences = Collections.unmodifiableSet (fences);
   128         this.occupied = computeOccupied (players, fences);
   129         
   130         for (Player p : players) {
   131             if (!accessibleFinalLine (p, p.endDirection, occupied, new BitSet ())) {
   132                 throw new IllegalPositionException ("Player " + p + " cannot reach " + p.endDirection + " side"); // NOI18N
   133             }
   134         }
   135         this.turn = turn % players.size();
   136     }
   137     
   138     /** Returns empty board with default starting position.
   139      * @return board with two pawns.
   140      */
   141     public static Board empty () {
   142         return new Board (8, 0, 10, 8, 16, 10);
   143     }
   144     
   145     /** Returns players in the game.
   146      * @return player object
   147      */
   148     public List<Player> getPlayers () {
   149         return players;
   150     }
   151     
   152     /** The fences currently on board. 
   153      * 
   154      * @return immutable set of Fences
   155      */
   156     public Set<Fence> getFences () {
   157         return fences;
   158     }
   159 
   160     /** The player that is supposed to play now.
   161      * @return the player to do next move
   162      */
   163     public Player getCurrentPlayer() {
   164         return players.get(turn);
   165     }
   166 
   167     /** The play who wins on current board.
   168      *
   169      * @return the winning player or <code>null</code>
   170      */
   171     public Player getWinner() {
   172         for (Player p : players) {
   173             if (p.endDirection.reached(p)) {
   174                 return p;
   175             }
   176         }
   177         return null;
   178     }
   179 
   180     /** Applies given move to current board.
   181      *
   182      * @param move move creates by one of the factory methods or fields of the {@link Move} class
   183      * @return new board derived from this one
   184      *
   185      * @throws cz.xelfi.quoridor.IllegalPositionException throws exception if the move is illegal
   186      */
   187     public Board apply(Move move) throws IllegalPositionException {
   188         if (getWinner() != null) {
   189             throw new IllegalPositionException("Game finished!"); // NOI18N
   190         }
   191 
   192         if (move.direction != null) {
   193             return move(getCurrentPlayer(), move.direction);
   194         } else {
   195             return fence(getCurrentPlayer(), move.fence);
   196         }
   197     }
   198 
   199     /** Can the move be applied to current board position?
   200      * 
   201      * @param move the move to apply
   202      * @return true if one can call {@link #apply} method without getting 
   203      *   an exception
   204      */
   205     public boolean isApplicable(Move move) {
   206         try {
   207             // trivial implementation is enough for now
   208             apply(move);
   209             return true;
   210         } catch (IllegalPositionException ex) {
   211             return false;
   212         }
   213     }
   214     
   215     /** Moves the player in given direction. The direction usually
   216      * is one of Player.Direction constants, but in case the move
   217      * is a jump over another players pawn it can be followed by
   218      * another (usually, if there is no fence the same) direction.
   219      *
   220      * @param player the player to move
   221      * @param where one or two directions saying where
   222      * @return the new board
   223      * @exception IllegalPositionException if the move is not possible
   224      */
   225     final Board move (Player player, Player.Direction... where) throws IllegalPositionException {
   226         if (where.length != 1 && where.length != 2) {
   227             throw new IllegalPositionException ("Move over one or two Directions"); // NOI18N
   228         }
   229         
   230         int index = players.indexOf (player);
   231         Player[] arr = players.toArray (new Player[0]);
   232 
   233         Player oneStep = newPosition (player, where[0]);
   234         
   235         if (where.length == 1) {
   236             arr[index] = oneStep;
   237             return new Board(turn + 1, Arrays.asList (arr), fences);
   238         }
   239 
   240         // straight jump over
   241         for (Player p : players) {
   242             if (p.getXInternal () == oneStep.getXInternal () && p.getYInternal() == oneStep.getYInternal ()) {
   243                 // ok, we are jumping over this one
   244                 GO_ON: if (where[0] != where[1]) {
   245                     // first of all ensure that we cannot go straight
   246                     try {
   247                         newPosition (oneStep, where[0]);
   248                     } catch (IllegalPositionException ex) {
   249                         // ok
   250                         break GO_ON;
   251                     }
   252                     throw new IllegalPositionException ("You have to jump straight if there is no wall"); // NOI18N
   253                 }
   254                 arr[index] = newPosition (oneStep, where[1]);
   255                 return new Board (turn + 1, Arrays.asList (arr), fences);
   256             }
   257         }
   258         throw new IllegalPositionException ("Cannot use multi direction when there is not oponent pawn"); // NOI18N
   259     }
   260     
   261     final Board fence (Player player, char x, int y, Fence.Orientation orientation) throws IllegalPositionException {
   262         return fence(player, new Fence ((x - 'A') * 2 + 1, y * 2 - 1, orientation));
   263     }
   264 
   265     private void columnLine(int width, int spaceX, Writer w) throws IOException {
   266         char ch = 'A';
   267         for (int x = 0; x < width - 1; x++) {
   268             if (x % (spaceX + 1) == 0 && x > 0) {
   269                 w.write(ch);
   270                 ch = (char) (ch + 1);
   271             } else {
   272                 w.write(' ');
   273             }
   274         }
   275     }
   276 
   277     private Board fence(Player player, Fence fence) throws IllegalPositionException {
   278         if (player.getFences () == 0) {
   279             throw new IllegalPositionException ("Not enough fences: " + player); // NOI18N
   280         }
   281         
   282         int index = players.indexOf (player);
   283         Player[] arr = players.toArray (new Player[0]);
   284         arr[index] = new Player (arr[index].getXInternal(), arr[index].getYInternal(), arr[index].getFences() - 1, arr[index].endDirection);
   285         
   286         HashSet<Fence> fen = new HashSet<Fence> (this.fences);
   287         fen.add (fence);
   288         
   289         return new Board (turn + 1, Arrays.asList (arr), fen);
   290     }
   291 
   292     //
   293     // Serialization
   294     //
   295 
   296     private static final Pattern northSouthPattern = Pattern.compile("(\\+(\\|*)(\\*)?-+\\+)");
   297 
   298     /** Reads the board from a reader. Opposite operation to {@link #write(java.io.Writer)}.
   299      *
   300      * @param r the reader
   301      * @return the read board
   302      * @throws IOException if I/O error occurs
   303      * @throws IllegalPositionException if the reader does not contain description
   304      *   of the board
   305      */
   306     public static Board read(Reader r) throws IOException, IllegalPositionException {
   307         BufferedReader b = new BufferedReader(r);
   308         for (;;) {
   309             String s = b.readLine();
   310             if (s == null) {
   311                 throw new IOException("No board found!");
   312             }
   313             Matcher m = northSouthPattern.matcher(s);
   314             if (m.find()) {
   315                 return readFromBetween(b, m);
   316             }
   317         }
   318     }
   319 
   320     /** Translates the string into board, if possible. String created by
   321      * use of {@link #toString()} is accepted, more information about the
   322      * format is avaliable in the description of {@link #write(java.io.Writer)}
   323      * method.
   324      *
   325      * @param board string to analyze
   326      * @return board object, if the string can be read
   327      * @throws IllegalPositionException if the string does not represent the board
   328      */
   329     public static Board valueOf(String board) throws IllegalPositionException {
   330         try {
   331             return read(new StringReader(board));
   332         } catch (IOException ex) {
   333             // shall not happen, StringReader does not throw IOException
   334             throw (IllegalPositionException)new IllegalPositionException(ex.getMessage()).initCause(ex);
   335         }
   336     }
   337 
   338     private static int assertChar(String s, int pos, char... ch) throws IOException {
   339         if (s.length() >= pos) {
   340             for (int i = 0; i < ch.length; i++) {
   341                 if (ch[i] == s.charAt(pos)) {
   342                     return i;
   343                 }
   344             }
   345         }
   346         throw new IOException("Not found " + ch[0] + " at " + pos + " in" + s);
   347     }
   348 
   349     private static Player findPlayer(
   350         Player previous, String line, int y, int spaceX, Player.Direction dir, int fences
   351     ) {
   352         int index = line.indexOf(dir.player);
   353         if (index == -1) {
   354             return previous;
   355         }
   356         int x = (index - 1) / (spaceX + 1) * 2;
   357         return new Player(x, y - 1, fences, dir);
   358     }
   359 
   360     private static Board readFromBetween(BufferedReader b, Matcher firstMatcher)
   361     throws IOException, IllegalPositionException {
   362         final int from = firstMatcher.start(1);
   363         final int to = firstMatcher.end(1);
   364         final int northFences = firstMatcher.end(2) - firstMatcher.start(2);
   365         final int spaceX = (to - from - 1) / 9 - 1;
   366         final int spaceY = 1;
   367 
   368         Player p = null;
   369         Player q = null;
   370         Set<Fence> fences = new HashSet<Fence>();
   371 
   372         StringBuffer sb = new StringBuffer();
   373         int row = 7;
   374         for (int y = (spaceY + 1) * 9 - 1; y > 0; y--) {
   375             String s = b.readLine();
   376             if (s == null) {
   377                 throw new EOFException();
   378             }
   379             sb.append(s);
   380             sb.append('\n');
   381             if (s.length() < to) {
   382                 throw new IOException("Too short line: " + s); // NOI18N
   383             }
   384             assertChar(s, from, '|');
   385             assertChar(s, to - 1, '|');
   386 
   387             if (y % (spaceY + 1) == 0) {
   388                 for (int x = 1; x < 9; x++) {
   389                     switch (assertChar(s, from + (spaceX + 1) * x, '+', '-', '|')) {
   390                         case 1:
   391                             fences.add(new Fence(x * 2 - 1, row * 2 + 1, Fence.Orientation.HORIZONTAL));
   392                             break;
   393                         case 2:
   394                             fences.add(new Fence(x * 2 - 1, row * 2 + 1, Fence.Orientation.VERTICAL));
   395                             break;
   396                         case 0:
   397                             break;
   398                         default:
   399                             assert false;
   400                     }
   401                 }
   402                 row--;
   403             } else {
   404                 String line = s.substring(from, to);
   405                 p = findPlayer(p, line, y, spaceX, Player.Direction.NORTH, -1);
   406                 q = findPlayer(q, line, y, spaceX, Player.Direction.SOUTH, northFences);
   407             }
   408         }
   409 
   410         String last = b.readLine();
   411         if (last == null) {
   412             throw new EOFException();
   413         }
   414         Matcher lastMatcher = northSouthPattern.matcher(last);
   415         if (!lastMatcher.find()) {
   416             throw new IOException("Unrecognized last line: " + last);
   417         }
   418 
   419         List<Player> arr = new ArrayList<Player>(2);
   420         assert p != null;
   421         int southFences = lastMatcher.end(2) - lastMatcher.start(2);
   422         arr.add(new Player(p.getXInternal(), p.getYInternal(), southFences, p.endDirection));
   423         arr.add(q);
   424         int turn = "*".equals(lastMatcher.group(3)) ? 0 : 1; // NOI18N
   425         return new Board(turn, arr, fences);
   426     }
   427 
   428 
   429     
   430     /** Writes the board to the provided writer. <b>P</b> denotes position
   431      * of the first player and <b>Q</b> of the second. Number of remaining
   432      * fences of each player are shown in left bottom and left top corner
   433      * as appropriate number of <b>|||</b> - one <b>|</b> per fence. The
   434      * player that is supposed to move in this turn has <b>*</b> right after
   435      * the set of its fences.
   436      *
   437      * <pre>
   438                          [N]
   439 
   440             A   B   C   D   E   F   G   H
   441             |   |   |   |   |   |   |   |
   442         +|||||------------------------------+
   443         |                 Q                 |
   444      8--|   +   +   +   +   +   +   +   +   |--8
   445         |       |                           |           
   446      7--|   +   |   +   +-------+-------+   |--7
   447         |       |       |                   |           
   448      6--|-------+-------|-------+-------+   |--6
   449         |               |                   |          
   450      5--|   +   +   +   +   +   +   +   +   |--5
   451 [W]     |               |                   |     [E]
   452      4--|   +   +   +   |   +   +   +-------|--4
   453         |               |                   |
   454      3--|   +   +   +   +-------+   +   +   |--3
   455         |                                   |
   456      2--|   +   +   +   +   +   +   +   +   |--2
   457         |                       |           |
   458      1--|   +   +   +   +   +   |   +   +   |--1
   459         |                 P     |           |
   460         +|||*-------------------------------+
   461             |   |   |   |   |   |   |   |
   462             A   B   C   D   E   F   G   H
   463 
   464                          [S]
   465      </pre>
   466      * @param w writer to write the board to
   467      * @exception IOException if communiction with writer fails
   468      */
   469     public void write (Writer w) throws IOException {
   470         write(w, 3, 1);
   471     }
   472 
   473     private void northSouthSign(int width, char sign, Writer w) throws IOException {
   474         int middle = width / 2;
   475         for (int x = 0; x < width; x++) {
   476             char ch = ' ';
   477             if (x == middle - 1) {
   478                 ch = '[';
   479             }
   480             if (x == middle) {
   481                 ch = sign;
   482             }
   483             if (x == middle + 1) {
   484                 ch = ']';
   485             }
   486             w.write(ch);
   487         }
   488         w.write(System.getProperty("line.separator")); // NOI18N
   489     }
   490 
   491     private void subColumnLine(int width, int spaceX, Writer w) throws IOException {
   492         for (int x = 0; x < width - 1; x++) {
   493             if (x % (spaceX + 1) == 0 && x > 0) {
   494                 w.write('|');
   495             } else {
   496                 w.write(' ');
   497             }
   498         }
   499     }
   500     
   501     /** This will print the board with provided spacing.
   502      * This is example of 3:1 spacing: 
   503      * <pre>
   504      *  +---+
   505      *  |sss|
   506      *  +---+
   507      * </pre>
   508      * and this 4:2 spacing:
   509      * <pre>
   510      *  +----+
   511      *  |ssss|
   512      *  |ssss|
   513      *  +----+
   514      * </pre>
   515      */
   516     private void write (Writer w, int spaceX, int spaceY) throws IOException {
   517         int width = spaceX * 9 + 10;
   518         int height = spaceY * 9 + 10;
   519         char[][] desk = new char[height][];
   520         for (int i = 0; i < height; i++) {
   521             desk[i] = new char[width];
   522             for (int j = 0; j < width; j++) {
   523                 if (i % (spaceY + 1) == 0 && j % (spaceX + 1) == 0) {
   524                     desk[i][j] = '+';
   525                 } else {
   526                     desk[i][j] = ' ';
   527                 }
   528             }
   529             desk[i][0] = '|';
   530             desk[i][width - 1] = '|';
   531         }
   532         for (int i = 1; i < width - 1; i++) {
   533             desk[0][i] = '-';
   534             desk[height -1][i] = '-';
   535         }
   536         desk[0][0] = '+';
   537         desk[0][width - 1] = '+';
   538         desk[height - 1][0] = '+';
   539         desk[height - 1][width - 1] = '+';
   540 
   541         {
   542             for (Player p : players) {
   543                 int px = p.getXInternal() / 2;
   544                 int py = p.getYInternal() / 2;
   545                 desk
   546                     [1 + py * (spaceY + 1) + spaceY / 2]
   547                     [1 + px * (spaceX + 1) + spaceX / 2] = p.endDirection.player;
   548                 paintLeftFences(desk, p.endDirection, p.getFences(), p.equals(getCurrentPlayer()));
   549             }
   550         }
   551         
   552         for (Fence f : fences) {
   553             int fx = (f.getX() / 2 + 1) * (spaceX + 1);
   554             int fy = (f.getY() / 2 + 1) * (spaceY + 1);
   555             switch (f.getOrientation()) {
   556                 case HORIZONTAL:
   557                     for (int i = -spaceX; i <= spaceX; i++) {
   558                         desk[fy][fx + i] = '-';
   559                     }
   560                     break;
   561                 case VERTICAL:
   562                     for (int i = -spaceY; i <= spaceY; i++) {
   563                         desk[fy + i][fx] = '|';
   564                     }
   565                     break;
   566                 default: 
   567                     throw new IllegalStateException ("Unknown orientation: " + f.getOrientation ()); // NOI18N
   568             }
   569         }
   570         w.write("        ");
   571         northSouthSign(width, 'N', w);
   572         w.write(System.getProperty("line.separator")); // NOI18N
   573         w.write("        ");
   574         columnLine(width, spaceX, w);
   575         w.write(System.getProperty("line.separator")); // NOI18N
   576         w.write("        ");
   577         subColumnLine(width, spaceX, w);
   578         w.write(System.getProperty("line.separator")); // NOI18N
   579         int cnt = 9;
   580         for (int y = height - 1; y >= 0; y--) {
   581             if (y == height / 2) {
   582                 w.write("[W]  ");
   583             } else {
   584                 w.write("     ");
   585             }
   586             boolean number = y % (spaceY + 1) == 0 && y > 0 && y < height - 1;
   587             if (number) {
   588                 cnt--;
   589                 w.write(Integer.toString(cnt));
   590                 w.write("--");
   591             } else {
   592                 w.write("   ");
   593             }
   594             for (int x = 0; x < width; x++) {
   595                 w.write(desk[y][x]);
   596             }
   597             if (number) {
   598                 w.write("--");
   599                 w.write(Integer.toString(cnt));
   600             } else {
   601                 w.write("   ");
   602             }
   603             if (y == height / 2) {
   604                 w.write("  [E]");
   605             }
   606             w.write(System.getProperty("line.separator")); // NOI18N
   607         }
   608         w.write("        ");
   609         subColumnLine(width, spaceX, w);
   610         w.write(System.getProperty("line.separator")); // NOI18N
   611         w.write("        ");
   612         columnLine(width, spaceX, w);
   613         w.write(System.getProperty("line.separator")); // NOI18N
   614         w.write(System.getProperty("line.separator")); // NOI18N
   615         w.write("        ");
   616         northSouthSign(width, 'S', w);
   617     }
   618 
   619 
   620     private void paintLeftFences(char[][] desk, Direction endDirection, int fences, boolean currentTurn) {
   621         assert fences >= 0 && fences <= 10 : "Players: " + players;
   622         switch (endDirection) {
   623             case SOUTH: {
   624                 for (int i = 0; i < fences; i++) {
   625                     desk[desk.length - 1][i + 1] = '|';
   626                 }
   627                 if (currentTurn) {
   628                     desk[desk.length - 1][fences + 1] = '*';
   629                 }
   630                 break;
   631             }
   632             case NORTH: {
   633                 for (int i = 0; i < fences; i++) {
   634                     desk[0][i + 1] = '|';
   635                 }
   636                 if (currentTurn) {
   637                     desk[0][fences + 1] = '*';
   638                 }
   639                 break;
   640             }
   641             default:
   642                 assert false;
   643         }
   644     }
   645 
   646 
   647     //
   648     // Standard Methods
   649     // 
   650 
   651     
   652     @Override
   653     public int hashCode () {
   654         return occupied.hashCode ();
   655     }
   656     
   657     @Override
   658     public boolean equals (Object o) {
   659         if (o instanceof Board) {
   660             Board b = (Board)o;
   661             return occupied.equals (b.occupied) && players.equals (b.players);
   662         }
   663         return false;
   664     }
   665 
   666     /** Converts the board into string representation. For more information
   667      * about the format see {@link #write(java.io.Writer)}. To read the
   668      * string back use {@link #valueOf(java.lang.String)}.
   669      *
   670      * @return string representing the board
   671      */
   672     @Override
   673     public String toString() {
   674         StringWriter w = new StringWriter();
   675         try {
   676             write(w);
   677         } catch (IOException ex) {
   678             return ex.toString();
   679         }
   680         return w.toString();
   681     }
   682 
   683     
   684     //
   685     // Validation methods
   686     //
   687 
   688     /** Computes new position of a player and checks whether there is no
   689      * fence between the old and new.
   690      */
   691     private Player newPosition (Player old, Player.Direction direction) throws IllegalPositionException {
   692         return newPosition (old, direction, occupied);
   693     }
   694     
   695     
   696     private static Player newPosition (Player old, Player.Direction direction, BitSet occupied) throws IllegalPositionException {
   697         int nx = old.x;
   698         int ny = old.y;
   699         int fx = old.x;
   700         int fy = old.y;
   701         
   702         switch (direction) {
   703             case NORTH: 
   704                 ny = old.y + 2;
   705                 fy = old.y + 1;
   706                 break;
   707             case SOUTH: 
   708                 ny = old.y - 2; 
   709                 fy = old.y - 1;
   710                 break;
   711             case EAST: 
   712                 nx = old.x + 2; 
   713                 fx = old.x + 1;
   714                 break;
   715             case WEST: 
   716                 nx = old.x - 2; 
   717                 fx = old.x - 1;
   718                 break;
   719             default: throw new IllegalStateException ("Unknown direction: " + direction); // NOI18N
   720         }
   721         
   722         if (nx < 0 || nx > 16) throw new IllegalPositionException ("Wrong player position: " + nx + ":" + ny); // NOI18N
   723         if (ny < 0 || ny > 16) throw new IllegalPositionException ("Wrong player position: " + nx + ":" + ny); // NOI18N
   724         
   725         int fenceIndex = toIndex (fx, fy);
   726         if (occupied.get (fenceIndex)) {
   727             throw new IllegalPositionException ("You cannot go over fences"); // NOI18N
   728         }
   729         
   730         return new Player (nx, ny, old.getFences (), old.endDirection);
   731     }
   732     
   733     /** @param position the current position of the player
   734      * @param endDir the side the player wants to reach
   735      * @param fences bits set to 1 when fences are placed
   736      * @param reached bits on squares that were already reached (modified during run)
   737      * @return true if the end line can be reached
   738      */
   739     private static boolean accessibleFinalLine (Player position, Player.Direction endDir, BitSet fences, BitSet reached) {
   740         int index = toIndex (position);
   741         if (reached.get (index)) {
   742             // already visited without success
   743             return false;
   744         }
   745         
   746         switch (endDir) {
   747             case NORTH: 
   748                 if (position.getYInternal() == 8) return true;
   749                 break;
   750             case SOUTH:
   751                 if (position.getYInternal() == 0) return true;
   752                 break;
   753             case EAST:
   754                 if (position.getXInternal() == 0) return true;
   755                 break;
   756             case WEST:
   757                 if (position.getXInternal() == 8) return true;
   758                 break;
   759             default:
   760                 throw new IllegalStateException ("Wrong direction: " + endDir); // NOI18N
   761         }
   762         
   763         reached.set (index);
   764         
   765         for (Player.Direction oneDirection : Player.Direction.values ()) {
   766             try {
   767                 if (accessibleFinalLine (newPosition (position, oneDirection, fences), endDir, fences, reached)) {
   768                     return true;
   769                 }
   770             } catch (IllegalPositionException ex) {
   771                 // ok, try once more
   772             }
   773         }
   774         
   775         return false;
   776     }
   777     
   778     /** Computes mask of the occupried bits.
   779      */
   780     private static BitSet computeOccupied (
   781         Collection<Player> players, Collection<Fence> fences
   782     ) throws IllegalPositionException {
   783         BitSet occupied = new BitSet (20 * 30); // TBD this is just an upper guess
   784         
   785         for (Player p : players) {
   786             if (p.getXInternal() % 2 == 1) throw new IllegalPositionException ("Wrong player position: " + p); // NOI18N
   787             if (p.getYInternal() % 2 == 1) throw new IllegalPositionException ("Wrong player position: " + p); // NOI18N
   788             
   789             int index = toIndex (p);
   790             if (occupied.get (index)) {
   791                 throw new IllegalPositionException ("There cannot be two players at " + p); // NOI18N
   792             }
   793             occupied.set (index);
   794         }
   795         
   796         for (Fence f : fences) {
   797             
   798             for (int i = -1; i <= 1; i++) {
   799                 int index = toIndex (f, i);
   800                 if (index < 0 || index > occupied.size ()) {
   801                     throw new IllegalPositionException ("Wrong fence position: " + f); // NOI18N
   802                 }
   803                 if (occupied.get (index)) {
   804                     throw new IllegalPositionException ("Fence collition: " + f); // NOI18N
   805                 }
   806                 
   807                 occupied.set (index);
   808             }
   809             
   810         }
   811         
   812         return occupied;
   813     }
   814 
   815     /** Converts twodimensional coordinates of player to one dimensional index.
   816      */
   817     private static int toIndex (Player p) {
   818         return toIndex (p.getXInternal(), p.getYInternal());
   819     }
   820     
   821     /** Converts twodimensional coordinates of fence to one dimensional index.
   822      * @param f the fence
   823      * @param whichPart (-1, 0, 1) 
   824      */
   825     private static int toIndex (Fence f, int whichPart) {
   826         int x = f.getX ();
   827         int y = f.getY ();
   828         
   829         switch (f.getOrientation ()) {
   830             case HORIZONTAL: x += whichPart; break;
   831             case VERTICAL: y += whichPart; break;
   832             default: throw new IllegalStateException ("Wrong orientation: " + f.getOrientation ()); // NOI18N
   833         }
   834         
   835         return toIndex (x, y);
   836     }
   837     
   838     /** Diagonal conversion of two positive integers to one integer.
   839      * <pre>
   840      * y3 9
   841      * y2 5  8
   842      * y1 2  4  7
   843      * y0 0  1  3  6
   844      *   x0 x1 x2 x3
   845      * </pre>
   846      */
   847     private static int toIndex (int x, int y) {
   848         int max = x + y;
   849         int baseOnY0 = max * (max + 1) / 2;
   850         return baseOnY0 + y;
   851     }
   852 
   853 }