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