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