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