1 /* CBFDatabase.java - Implementation of old-style chessbase database.
2 *
3 * $Header: M:\\CVSWortel/foobase/net/sourceforge/foobase/CBFDatabase.java,v 1.3 2001/10/07 18:07:41 eddy Exp $
4 *
5 * Copyright (c) 2000 Eddy Vanlerberghe. All rights reserved.
6 *
7 * Redistribution and use in source and binary forms, with or without
8 * modification, are permitted provided that the following conditions
9 * are met:
10 * 1. Redistributions of source code must retain the above copyright
11 * notice, this list of conditions and the following disclaimer.
12 * 2. Redistributions in binary form must reproduce the above copyright
13 * notice, this list of conditions and the following disclaimer in the
14 * documentation and/or other materials provided with the distribution.
15 * 3. The name of Eddy Vanlerberghe shall not be used to endorse or promote
16 * products derived from this software without specific prior written
17 * permission.
18 *
19 * THIS SOFTWARE IS PROVIDED BY EDDY VANLERBERGHE ``AS IS'' AND
20 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
21 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
22 * ARE DISCLAIMED. IN NO EVENT SHALL EDDY VANLERBERGHE BE LIABLE
23 * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
24 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
25 * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
26 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
27 * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
28 * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
29 * SUCH DAMAGE.
30 *
31 * Last modification by: $Author: eddy $
32 * Last modification on: $Date: 2001/10/07 18:07:41 $
33 *
34 * File Revision: $Revision: 1.3 $
35 * File History:
36 * $Log: CBFDatabase.java,v $
37 * Revision 1.3 2001/10/07 18:07:41 eddy
38 * - Implemented update() and delete() methods
39 * - Fixed a problem with tournament input parsing
40 *
41 * Revision 1.2 2000/12/16 20:39:00 eddy
42 * Cleaned up code a bit.
43 *
44 * Revision 1.1.1.1 2000/12/15 16:27:17 eddy
45 * First halfway acceptable version.
46 *
47 *****************************************************************************/
48 package net.sourceforge.foobase;
49
50 import java.io;
51 import java.util;
52
53 /**
54 * Implementation of old-style chessbase database.
55 * <p>
56 * This class is based on the sources of "CB Utilities" by
57 * Andy Duplain (version of 1-Apr-96, obtained from
58 * the <a hfref="ftp://ftp.pitt.edu">Pittsburg University</a> FTP site)
59 * <p>
60 * None of the code made it directly in this tool (obviously, because the
61 * original was written C ;-) but nonetheless, much of the information on
62 * which this source is based on, comes from that toolset.
63 * <p>
64 * The structure of the index file (<CODE>.CBI</CODE> file) is like this:
65 * <p>
66 * <table>
67 * <tr><th>Byte-offset</th><th>Length in bytes</th><th>Contents</th></tr>
68 * <tr><td>0</td><td>4</td><td>Number of games + 1</td></tr>
69 * <tr><td>...</td><td>...</td><td>...</td></tr>
70 * <tr><td>4 * (i + 1)</td><td>4</td><td>Location of game i (first game
71 * is 0) + (i + 2)</td></tr>
72 * <tr><td>...</td><td>...</td><td>...</td></tr>
73 * </table>
74 * <p>
75 * The last entry in the index table contains the byte offset in the games
76 * datafile for the next game to enter. E.g. if the database contains 5 games,
77 * the index file will contain at byte offset 24 (next-game-index ( = 5 + 1)
78 * times 4 (length of entries in index file)) the 32-bit value
79 * (byte-length-games-file) + 12 (= 2 times index of next game (-= 5 + 1))
80 * <p>
81 * The data in the games file is seriously obfuscated, but the table below
82 * gives the logical structure without regard for the encoding schemes.
83 * <p>
84 * Each game is structured like this:
85 * <p>
86 * <ul>
87 * <li>14 header bytes
88 * <li>players information
89 * <li>source information
90 * <li>actual moves of game encoded like this:
91 * <p>
92 * <table border="1">
93 * <tr><th>Byte-value</th><th>Description</th></tr>
94 * <tr><td>0xff</td><td>Marks start of a new variation</td></tr>
95 * <tr><td>0x80</td><td>Marks end of variation</td></tr>
96 * <tr><td>0x01 - 0x7E</td><td>Number of move from movelist
97 * (first move is 1)</td></tr>
98 * <tr><td>0x81 - 0xFE</td><td>Move has comments attached to it: clear high
99 * bit to obtain regular move number (first move is 1)</td></tr>
100 * </table>
101 * <p>
102 * <li>comments: are encoded in a variable length structure. Each structure
103 * starts with byte 0xFF and continues up to just before the next byte with
104 * that value (or until the end of available comment bytes). The bytes contain
105 * the ASCII values of the annotation text, with the exception of a set of
106 * special characters that are defined in chessbase specific fonts (mostly
107 * figurines and evaluation symbols) See also <CODE>CBFUtil</CODE>.
108 * The list of fields given below may end at any given moment at
109 * the marker for the next comment block:
110 * <p>
111 * <table border="1">
112 * <tr><th>Byte-length</th><th>Description</th></tr>
113 * <tr><td>1</td><td>evaluation string (e.g. "blunder")</td></tr>
114 * <tr><td>1</td><td>position evalutation string
115 * (e.g. "with compensation")</td></tr>
116 * <tr><td>1</td><td>move evaluation string (e.g.
117 * "better is")</td></tr>
118 * <tr><td>1</td><td>unknown for now: these bytes appear to be
119 * <CODE>0x00</CODE></td></tr>
120 * <tr><td>up to just before next 0xFF byte</td><td>Annotation string to
121 move</td></tr>
122 * </table>
123 * <p>
124 * In order to keep things interesting, there is a feature to store annotated
125 * text on the game level. If present, such comment is appended to the
126 * "regular" comments for the game.
127 * <p>
128 * <li>initial setup: contains exactly 33 bytes. The first 32 bytes contain
129 * two board cells each (each cell requires 4 bits) and the last byte contains
130 * the number of (whole) moves done to reach the specified position. The cells
131 * are specified row by row and the high nibble goes first. See
132 * <CODE>Board</CODE> for information on piece encoding in the cells.
133 * </ul>
134 *
135 * @version $Revision: 1.3 $
136 * @author $Author: eddy $
137 */
138 public class CBFDatabase implements IsGameDatabase
139 {
140 // {{ Class members }}
141
142 /**
143 * Switch for toggling debug output.
144 */
145 private final static boolean VERBOSE = false;
146 /**
147 * Table for hexadecimal conversion
148 */
149 private final static char[] HEX_DIGITS = {'0', '1', '2', '3', '4', '5',
150 '6', '7', '8', '9', 'A', 'B',
151 'C', 'D', 'E', 'F'};
152 /**
153 * Suffix for index file.
154 */
155 private final static String IDX_TYPE = ".cbi";
156 /**
157 * Suffix for moves (games) file.
158 */
159 private final static String IDX_GAMES = ".cbf";
160 /**
161 * Position in indexfile of (number-of-games + 1)
162 */
163 private final static long OFSIDX_NEXT_GAME_IDX = 0;
164 /**
165 * Length in bytes of game header.
166 */
167 private final static int HEADER_LEN = 14;
168 /**
169 * Length for initial board setup.
170 * <p>
171 * Note that initial board setups are always of the same length.
172 */
173 private final static int STARTPOS_LEN = 33;
174 /**
175 * Start of variationn marker
176 */
177 private final static int START_OF_VARIATION = 0xff;
178 /**
179 * End of variation marker
180 */
181 private final static int END_OF_VARIATION = 0x80;
182 /**
183 * End of annotation block marker
184 */
185 private final static int END_COMMENT = 0xff;
186 /**
187 * Move generator to use for old-style chessbase databases
188 */
189 private static CBFMoveGenerator _dynamo = null;
190
191 // {{ Instance members }}
192
193 /**
194 * Name of the database.
195 * <p>
196 * This name contains both the location (directory) and the pure name
197 * part of the database.
198 */
199 protected String _dbnam;
200 /**
201 * Index file.
202 */
203 protected RandomAccessFile _idx;
204 /**
205 * Games data file.
206 */
207 protected RandomAccessFile _gms;
208
209 // {{ Class methods }}
210
211 /**
212 * Log a message.
213 *
214 * @param s <CODE>String</CODE> to log
215 */
216 private static void say(String s) {
217 if (VERBOSE) {
218 System.err.println("CBFDatabase: " + s);
219 System.err.flush();
220 }
221 }
222 /**
223 * Convert a byte array containing ASCII characters to string format.
224 *
225 * @param b <CODE>byte[]</CODE> to convert
226 * @param ofs <CODE>int</CODE> containing the offset to start from
227 * @param int <CODE>int</CODE> containing the number of bytes to convert
228 * @return <CODE>String</CODE> containing the converted bytes.
229 */
230 private static String b2s(byte[] b, int ofs, int len) {
231 StringBuffer sb = new StringBuffer(len);
232 for (int i = 0; i < len; ++i) {
233 sb.append((char) b[i + ofs]);
234 }
235 return sb.toString();
236 }
237 /**
238 * Convert a byte array containing ASCII characters to string format.
239 *
240 * @param b <CODE>byte[]</CODE> to convert
241 * @return <CODE>String</CODE> containing the converted bytes.
242 */
243 private static String b2s(byte[] b) {
244 return b2s(b, 0, b.length);
245 }
246 /**
247 * Convert a byte array to hex format.
248 *
249 * @param b <CODE>byte[]</CODE> to convert
250 * @param ofs <CODE>int</CODE> containing the offset to start from
251 * @param int <CODE>int</CODE> containing the number of bytes to convert
252 * @return <CODE>String</CODE> containing the converted bytes.
253 */
254 private static String b2h(byte[] b, int ofs, int len) {
255 StringBuffer sb = new StringBuffer(len);
256 for (int i = 0; i < len; ++i) {
257 int c = (int) b[i + ofs];
258 if (i > 0) {
259 sb.append(' ');
260 }
261 sb.append(HEX_DIGITS[(c >> 4) & 0x0f]);
262 sb.append(HEX_DIGITS[c & 0x0f]);
263 }
264 return sb.toString();
265 }
266 /**
267 * Convert a byte array to hex format.
268 *
269 * @param b <CODE>byte[]</CODE> to convert
270 * @return <CODE>String</CODE> containing the converted bytes.
271 */
272 private static String b2h(byte[] b) {
273 return b2h(b, 0, b.length);
274 }
275 /**
276 * Get single instance of <CODE>CBFMoveGenerator</CODE> class to use
277 * for move generation.
278 *
279 * @return <CODE>CBFMoveGenerator</CODE> to use for old-style chessbase
280 * databases
281 */
282 public static CBFMoveGenerator getMoveGenerator() {
283 if (_dynamo == null) {
284 _dynamo = new CBFMoveGenerator();
285 }
286 return _dynamo;
287 }
288
289 // {{ Constructors }}
290
291 /**
292 * Default public constructor.
293 */
294 public CBFDatabase()
295 {
296 _dbnam = null;
297 _idx = null;
298 _gms = null;
299 }
300
301 // {{ Instance methods }}
302
303 /**
304 * Set name of Game collection.
305 * <p>
306 * The meaning of this name is implementation dependent, that is, it is
307 * up to the implementing classes to interprete this name.
308 * <p>
309 * For example, this may be the name of a PGN file or it may be the
310 * basename for an old-style Chessbase database (that will use files with
311 * this basename followed by the extensions ".CBI" and
312 * ".CBF")
313 *
314 * @param nam <CODE>String</CODE> specifying the name of the database.
315 * @exception java.lang.Exception is thrown whenever the name is not
316 * acceptable.
317 */
318 public void setName(String nam) throws Exception {
319 _dbnam = nam;
320 }
321 /**
322 * Create a new database.
323 * <p>
324 * Note that if the database already exists, its index pointers will be
325 * reset, thus effectively emptying the database.
326 *
327 * @exception java.lang.Exception if the database could not be created.
328 */
329 public void create() throws Exception {
330 open(); // Will create empty files in case they do not yet exist
331 barfInt(_idx, (long) 0, 1); // Index next game
332 barfInt(_idx, (long) 4, 2); // Encoded data offset next game
333 }
334 /**
335 * Open an existing database.
336 *
337 * @exception java.lang.Exception if the database could not be opened.
338 */
339 public void open() throws Exception {
340 if (_dbnam == null) {
341 throw new Exception("CBFDatabase.open() -- " +
342 "missing database name.");
343 }
344 _idx = new RandomAccessFile(_dbnam + IDX_TYPE, "rw");
345 _gms = new RandomAccessFile(_dbnam + IDX_GAMES, "rw");
346 }
347 /**
348 * Returns the number of Games stored in the database.
349 *
350 * @return <CODE>int</CODE> containing the number of Games.
351 * @exception java.lang.Exception if an error occured during the operation.
352 */
353 public int size() throws Exception {
354 return (slurpInt(_idx, OFSIDX_NEXT_GAME_IDX) - 1);
355 }
356 /**
357 * Retrieve all games present in the database.
358 *
359 * @return <CODE>Vector</CODE> containing the games present in the
360 * database.
361 * @exception java.lang.Exception if an error occured during the operation.
362 */
363 public Vector getGames() throws Exception {
364 Vector v = new Vector();
365 int n = size();
366 for (int i = 0; i < n; ++i) {
367 v.addElement(get(i));
368 }
369 return v;
370 }
371 /**
372 * Retrieve the headers for all games present in the database.
373 *
374 * @return <CODE>Vector</CODE> containing the header strings for all
375 * the games present in the database.
376 * @exception java.lang.Exception if an error occured during the operation.
377 */
378 public Vector getHeaders() throws Exception {
379 Vector v = new Vector();
380 int n = size();
381 for (int i = 0; i < n; ++i) {
382 String hdr = getHeader(i);
383 if (hdr != null) {
384 v.addElement(hdr);
385 }
386 }
387 return v;
388 }
389 /**
390 * Retrieve a game header text based on its index in the database.
391 * <p>
392 * The information retrieved by this method serves mainly to display
393 * an overview of the games stored in a database.
394 * <p>
395 * Contrary to the <CODE>get()</CODE> method, this method does not throw
396 * an exception if the specified game is deleted.
397 *
398 * @param idx <CODE>int</CODE> zero-based index of the game to retrieve
399 * @return <CODE>String</CODE> containing the header information of the
400 * specified game (index number, name of white, name of black) or
401 * <CODE>null</CODE> in case the game is deleted.
402 * @exception java.lang.Exception if an error occured during the operation.
403 */
404 public String getHeader(int idx) throws Exception {
405 if ((idx < 0) || (idx >= size())) {
406 throw new IndexOutOfBoundsException("No game with index " + idx);
407 }
408 //
409 // Offset in games file of game data
410 //
411 int go = slurpInt(_idx, (long) ((idx + 1) * 4)) - idx - 2;
412 //
413 // Grab game header bytes
414 //
415 byte[] hdr = slurp(_gms, (long) go, HEADER_LEN);
416 CBFUtil.decodeHeader(hdr);
417 if ((hdr[10] & 0x80) != 0) {
418 //
419 // Game is deleted.
420 //
421 return null;
422 }
423 int res = (int) hdr[1] & 0xff;
424 int year = extractYear(hdr);
425 int welo = extractWhiteELO(hdr);
426 int belo = extractBlackELO(hdr);
427 String eco = extractECO(hdr);
428 int lenplayers = (int) hdr[4] & 0x3f;
429 int lensource = (int) hdr[5] & 0x3f;
430 //
431 // Grab players and so called "source".
432 //
433 int ho = go + HEADER_LEN;
434 byte[] hdrtxt = slurp(_gms, (long) ho, lenplayers + lensource);
435 CBFUtil.decodeHeaderText(hdrtxt);
436 String[] infofields = parseHeaderText(hdrtxt, lenplayers);
437 StringBuffer sb = new StringBuffer();
438 sb.append(idx);
439 sb.append(' ');
440 if (infofields[2] != null) {
441 sb.append(infofields[2]);
442 }
443 sb.append('-');
444 if (infofields[3] != null) {
445 sb.append(infofields[3]);
446 }
447 if (infofields[4] != null) {
448 sb.append('[');
449 sb.append(infofields[4]);
450 if (infofields[0] != null) {
451 sb.append('(');
452 sb.append(infofields[0]);
453 sb.append(')');
454 }
455 sb.append(']');
456 }
457 sb.append(' ');
458 sb.append(year);
459 sb.append(' ');
460 switch (res) {
461 case 0:
462 sb.append("0-1");
463 break;
464 case 1:
465 sb.append("1/2");
466 break;
467 case 2:
468 sb.append("1-0");
469 break;
470 default:
471 sb.append("???");
472 break;
473 }
474 return sb.toString();
475 }
476 /**
477 * Retrieve a game based on its index in the database.
478 *
479 * @param idx <CODE>int</CODE> zero-based index of the game to retrieve
480 * @return <CODE>Game</CODE> containing the specified game
481 * @exception java.lang.Exception if an error occured during the operation.
482 */
483 public Game get(int idx) throws Exception {
484 if ((idx < 0) || (idx >= size())) {
485 throw new IndexOutOfBoundsException("No game with index " + idx);
486 }
487 //
488 // Offset in games file of game data
489 //
490 int go = slurpInt(_idx, (long) ((idx + 1) * 4)) - idx - 2;
491 //
492 // Grab game header bytes
493 //
494 byte[] hdr = slurp(_gms, (long) go, HEADER_LEN);
495 CBFUtil.decodeHeader(hdr);
496 if ((hdr[10] & 0x80) != 0) {
497 //
498 // Game is deleted.
499 //
500 throw new Exception("Game " + idx + " is deleted.");
501 }
502 Game g = new Game();
503 g.setID(new CBFGameID(this, idx));
504 parseHeader(hdr, g);
505 int lenplayers = (int) hdr[4] & 0x3f;
506 int lensource = (int) hdr[5] & 0x3f;
507 int lenmoves = (((int) hdr[2] &0xff) << 8 |
508 ((int) hdr[3] &0xff)) - 1;
509 int lencomm = ((int) hdr[6] &0xff) << 8 | ((int) hdr[7] &0xff);
510 //
511 // Grab players and so called "source".
512 //
513 int ho = go + HEADER_LEN;
514 byte[] hdrtxt = slurp(_gms, (long) ho, lenplayers + lensource);
515 CBFUtil.decodeHeaderText(hdrtxt);
516 parseHeaderText(hdrtxt, lenplayers, g);
517 int mo = ho + lenplayers + lensource;
518 byte[] moves = null;
519 if (lenmoves > 0) {
520 moves = slurp(_gms, (long) mo, lenmoves);
521 } else {
522 moves = new byte[0];
523 }
524 //
525 // Process all available annotation data.
526 //
527 Vector notes = null;
528 int co = mo + lenmoves;
529 if (lencomm > 0) {
530 byte[] comms = slurp(_gms, (long) co, lencomm);
531 notes = CBFAnnotation.parse(comms);
532 } else {
533 notes = new Vector();
534 }
535 Board board = new Board();
536 if ((hdr[10] & 0x01) != 0) {
537 //
538 // Grab initial board setup
539 //
540 int epmask = ((int) hdr[11]) & 0x0f;
541 int ipo = co + lencomm;
542 byte[] bar = slurp(_gms, (long) ipo, STARTPOS_LEN);
543 board.clear();
544 int k = 0;
545 for (int i = 0; i < 8; ++i) {
546 for (int j = 0; j < 8; ++j) {
547 byte pies = bar[k++];
548 board.setPiece((byte) ((pies >> 4) & 0x0f), i, j++);
549 board.setPiece((byte) (pies & 0x0f), i, j);
550 }
551 }
552 board.setEP(epmask);
553 if (g.isBlackToMove()) {
554 board.setNextHalfMoveNumber(2 * ((int) bar[32] + 1));
555 } else {
556 board.setNextHalfMoveNumber(2 * ((int) bar[32] + 1) - 1);
557 }
558 g.setHasInitialBoard(true);
559 } else {
560 //
561 // Use normal initial game setup.
562 //
563 g.setHasInitialBoard(false);
564 board.initialSetup();
565 }
566 g.setInitialPosition(board);
567 //
568 // The big one: parse moves and comments
569 //
570 processMoves(g, moves, notes);
571 return g;
572 }
573 /**
574 * Add a new Game to the database.
575 * <p>
576 * This method is not required to verify insertion of duplicate Games.
577 * <p>
578 * Note that the input parameter is changed: its game ID is set.
579 *
580 * @param g <CODE>Game</CODE> to add to the database.
581 * @exception java.lang.Exception if an error occured during the operation.
582 */
583 public void add(Game g) throws Exception {
584 //
585 // Phase one: prepare byte arrays to dump in the games file.
586 //
587 byte[] hdr = new byte[HEADER_LEN];
588 byte[] data = prepareGameForOutput(g, hdr);
589 //
590 // Phase two: update both index and games file.
591 //
592 // Index next game (1-based)
593 //
594 int idx = slurpInt(_idx, OFSIDX_NEXT_GAME_IDX);
595 //
596 // Stuff game ID in Game object
597 //
598 g.setID(new CBFGameID(this, idx - 1));
599 //
600 // Offset in games file where to write next game
601 //
602 // Note: do not subtract 2 for game offset calculation (this ngidx
603 // is 1-based in stead of zero based!)
604 //
605 int ngidx = idx;
606 int ngofs = slurpInt(_idx, (long) (ngidx * 4)) - ngidx - 1;
607 barf(_gms, (long) ngofs, hdr, 0, hdr.length);
608 barf(_gms, (long) (ngofs + hdr.length), data, 0, data.length);
609 ++ngidx;
610 ngofs += hdr.length + data.length + ngidx + 1;
611 barfInt(_idx, (long) 0, ngidx);
612 barfInt(_idx, (long) (ngidx * 4), ngofs);
613 }
614 /**
615 * Prepare buffers to be written to the database.
616 * <p>
617 * This method uses a somewhat dubious technique to return two
618 * pieces of data (well, two byte arrays): one is a fixed size
619 * array that needs to be pre-allocated and passed to the function as
620 * an argument, the other one is the function result.
621 *
622 * @param g <CODE>Game</CODE> to add to the database.
623 * @param hdr <CODE>byte[]</CODE> to store the header information in
624 * (this buffer should contain <CODE>HEADER_LEN</CODE> bytes)
625 * @return <CODE>byte[]</CODE> containing the data to write to the
626 * database file (including everything but the
627 * kitchen sink^H^H^H^H^H^H^H^H^H^H^H^H header)
628 * @exception java.lang.Exception if an error occured during the operation.
629 */
630 protected byte[] prepareGameForOutput(Game g, byte[] hdr)
631 throws Exception {
632 //
633 // For safety reasons it would seem better to first build the entire
634 // byte array for the game in memory and only when all conversions
635 // are completed successfully, the array is dumped in file in one
636 // single swoop.
637 //
638 // All intermediate operations are done on this byte array stream.
639 //
640 ByteArrayOutputStream bos = new ByteArrayOutputStream();
641 byte[] players = buildPlayersInfo(g);
642 byte[] source = buildSourceInfo(g);
643 byte[] hdrtxt = new byte[players.length + source.length];
644 System.arraycopy(players, 0, hdrtxt, 0, players.length);
645 System.arraycopy(source, 0, hdrtxt, players.length, source.length);
646 CBFUtil.decodeHeaderText(hdrtxt);
647 bos.write(hdrtxt);
648 ByteArrayOutputStream mvs = new ByteArrayOutputStream();
649 ByteArrayOutputStream cms = new ByteArrayOutputStream();
650 cms.write(END_COMMENT); // Write initial marker
651 if (g.getMoves() != null) {
652 appendMoves(mvs, cms, g.getMoves(), g.getInitialPosition(),
653 g.getInitialPosition().getNextHalfMoveNumber());
654 }
655 byte[] mvbyts = mvs.toByteArray();
656 byte[] cmbyts = cms.toByteArray();
657 //
658 // Obfuscate moves ...
659 //
660 int l = mvbyts.length + 1;
661 int mask = l * 49;
662 int i = 0;
663 for (i = l - 2; i > 0; --i) {
664 mvbyts[i] ^= mask;
665 mask *= 7;
666 }
667 bos.write(mvbyts);
668 bos.write(cmbyts);
669 //
670 // Time to build the game header info.
671 //
672 for (i = 0; i < HEADER_LEN; ++i) {
673 hdr[i] = (byte) 0; // Zero out header first.
674 }
675 Date dat = g.getDate();
676 if (dat == null) {
677 hdr[0] = (byte) 0x7f;
678 } else {
679 //
680 // Store year - 1900
681 //
682 hdr[0] = (byte) dat.getYear();
683 }
684 hdr[1] = (byte) g.getResult();
685 hdr[2] = (byte) (((mvbyts.length + 1) >> 8) & 0xff);
686 hdr[3] = (byte) ((mvbyts.length + 1) & 0xff);
687 char ecoletter = g.getECOLetter();
688 int ecomain = g.getECO();
689 int eco1 = ((int) ecoletter - (int) 'A') * 100 + ecomain + 1;
690 hdr[4] = (byte) (((eco1 & 0x60) << 1) | (players.length & 0x3f));
691 hdr[5] = (byte) (((eco1 & 0x180) >> 1) | (source.length & 0x3f));
692 hdr[6] = (byte) ((cmbyts.length >> 8) & 0xff);
693 hdr[7] = (byte) (cmbyts.length & 0xff);
694 int we = g.getWhiteELO();
695 int be = g.getBlackELO();
696 //
697 // Note: this database format does not allow ELO ratings to
698 // be less than 1600. So, *IF* a rating is given we force it to
699 // be at least 1600.
700 //
701 if (we != 0) {
702 hdr[8] = (byte) ((((we >= 1600) ? we : 1600) - 1600) / 5);
703 }
704 if (be != 0) {
705 hdr[9] = (byte) ((((be >= 1600) ? be : 1600) - 1600) / 5);
706 }
707 //
708 // WARNING: serious foefeling ahead (on the part of chessbase)
709 // If an initial game setup was given for a game, it is assumed that
710 // no ECO info is available and hence some bits can be recycled for
711 // other purposes. Bit 0x02 of hdr[10] is used either to store which
712 // player is to move first _OR_ to hold info about the ECO encoding.
713 //
714 if (g.hasInitialSetup()) {
715 int c = (g.isBlackToMove() ? 2 : 0);
716 hdr[10] = (byte) (c | 0x01);
717 Board brd = g.getInitialPosition();
718 hdr[11] = (byte) brd.getEP();
719 byte[] cells = brd.getBoard();
720 byte[] bar = new byte[STARTPOS_LEN];
721 int k = 0;
722 for (i = 0; i < 8; ++i) {
723 for (int j = 0; j < 8; ++j) {
724 c = (cells[(j++ << 3) | i] << 4) | (cells[(j << 3) | i]);
725 bar[k++] = (byte) c;
726 }
727 }
728 int mvn = brd.getNextHalfMoveNumber();
729 if (g.isBlackToMove()) {
730 cells[32] = (byte) (mvn / 2 - 1);
731 } else {
732 cells[32] = (byte) (((mvn + 1) / 2) - 1);
733 }
734 bos.write(cells);
735 } else {
736 hdr[10] = (byte) ((eco1 & 0x1f) << 1);
737 //
738 // Note: it is not clear what bit 0x40 of hdr[11] is used for.
739 //
740 int subeco = g.getSubECO();
741 if (subeco != 0) {
742 hdr[11] = (byte) (((subeco & 0x40) << 1) | (subeco & 0x3f));
743 }
744 }
745 hdr[12] = (byte) g.getNumberMoves();
746 CBFUtil.setChecksum(hdr);
747 CBFUtil.encodeHeader(hdr);
748 //
749 // All went according to plan: bail out.
750 //
751 return bos.toByteArray();
752 }
753 /**
754 * Update a Game in the database.
755 * <p>
756 * Note that this method is only required to work on Games that were
757 * previously read from the same database as the update is being done.
758 *
759 * @param g <CODE>Game</CODE> to update in the database.
760 * @exception java.lang.Exception if an error occured during the operation.
761 */
762 public void update(Game g) throws Exception {
763 //
764 // First determine if this game contains a valid ID for the current
765 // database.
766 //
767 int gmidx = getGameID(g);
768 if(gmidx < 0) {
769 throw new Exception("CBFDatabase.update() -- " +
770 "Game does not come from this database.");
771 }
772 //
773 // Ok, game is part of this database.
774 //
775 // Phase one: prepare byte arrays to dump in the games file.
776 //
777 byte[] hdr = new byte[HEADER_LEN];
778 byte[] data = prepareGameForOutput(g, hdr);
779 //
780 // Phase two: update both index and games file.
781 //
782 //
783 // Index next game (1-based)
784 //
785 int ngidx = slurpInt(_idx, OFSIDX_NEXT_GAME_IDX);
786 //
787 // Offset in games file where to write next game
788 //
789 // Note: do not subtract 2 for game offset calculation (this ngidx
790 // is 1-based in stead of zero based!)
791 //
792 int ngofs = slurpInt(_idx, (long) (ngidx * 4)) - ngidx - 1;
793 barf(_gms, (long) ngofs, hdr, 0, hdr.length);
794 barf(_gms, (long) (ngofs + hdr.length), data, 0, data.length);
795 //
796 // Update pointer in games file for this game index
797 //
798 barfInt(_idx, (long) ((gmidx + 1) * 4), ngofs + gmidx + 2);
799 //++ngidx;
800 ngofs += hdr.length + data.length + ngidx + 1;
801 //barfInt(_idx, (long) 0, ngidx);
802 barfInt(_idx, (long) (ngidx * 4), ngofs);
803 }
804 /**
805 * Remove a Game from the database.
806 * <p>
807 * Note that this method is only required to work on Games that were
808 * previously read from the same database as the update is being done.
809 *
810 * @param g <CODE>Game</CODE> to remove from the database.
811 * @exception java.lang.Exception if an error occured during the operation.
812 */
813 public void delete(Game g) throws Exception {
814 //
815 // First determine if this game contains a valid ID for the current
816 // database.
817 //
818 int idx = getGameID(g);
819 if(idx < 0) {
820 throw new Exception("CBFDatabase.delete() -- " +
821 "Game does not come from this database.");
822 }
823 //
824 // Ok, game is part of this database.
825 //
826 // Offset in games file of game data
827 //
828 int go = slurpInt(_idx, (long) ((idx + 1) * 4)) - idx - 2;
829 //
830 // Grab game header bytes
831 //
832 byte[] hdr = slurp(_gms, (long) go, HEADER_LEN);
833 CBFUtil.decodeHeader(hdr);
834 hdr[10] |= (byte) 0x80; // Force delete flag
835 CBFUtil.setChecksum(hdr);
836 CBFUtil.encodeHeader(hdr);
837 barf(_gms, (long) go, hdr, 0, HEADER_LEN);
838 }
839 /**
840 * Close a database.
841 *
842 * @exception java.lang.Exception if an error occured during the operation.
843 */
844 public void close() throws Exception {
845 if ((_idx == null) || (_gms == null)) {
846 throw new Exception("CBFDatabase.close() -- not properly opened.");
847 }
848 _idx.close();
849 _gms.close();
850 _idx = null;
851 _gms = null;
852 }
853 /**
854 * Return the game ID of the specified game in this database.
855 *
856 * @param g <CODE>Game</CODE> to test for its presence in this database
857 * @return <CODE>int</CODE> containing the internal game ID in this
858 * database for the specified game, or <CODE>-1</CODE> if the game is
859 * not stored in this database.
860 */
861 public int getGameID(Game g) {
862 int gmidx = -1;
863 Vector ids = g.getIDs();
864 if(ids != null) {
865 for (Enumeration e = ids.elements(); e.hasMoreElements(); ) {
866 IsGameID theid = (IsGameID) e.nextElement();
867 if(theid instanceof CBFGameID) {
868 CBFGameID cbfid = (CBFGameID) theid;
869 if(cbfid.getDatabase() == this) {
870 gmidx = cbfid.getGameID();
871 break;
872 }
873 }
874 }
875 }
876 return gmidx;
877 }
878 /**
879 * Extract all relevant information from the game header bytes.
880 *
881 * @param hdr <CODE>byte[]</CODE> containing the decoded header bytes read
882 * from the database
883 * @param g <CODE>Game</CODE> to initialize with the extracted header
884 * information
885 */
886 protected void parseHeader(byte[] hdr, Game g) {
887 g.setResult((int) hdr[1] & 0xff);
888 //
889 // Old-style databases only keep the year, not the entire date
890 //
891 int y = ((int) hdr[0]) & 0xff;
892 if (y != 127) {
893 g.setDate(new Date(y, 0, 1));
894 }
895 g.setBlackMoves((hdr[10] & 0x02) != 0);
896 if (hdr[8] != 0) {
897 g.setWhiteELO(1600 + 5 * (((int) hdr[8]) & 0xff));
898 }
899 if (hdr[9] != 0) {
900 g.setBlackELO(1600 + 5 * (((int) hdr[9]) & 0xff));
901 }
902 int e = ((((int) hdr[10]) & 0x3e) >> 1) |
903 ((((int) hdr[4]) & 0xc0) >> 1) |
904 ((((int) hdr[5]) & 0xc0) << 1);
905 g.setECOLetter((char) ((int) 'A' + ((e - 1) / 100)));
906 g.setECO((e - 1) %100);
907 g.setSubECO((((int) hdr[11]) & 0x3f) |
908 ((((int) hdr[11]) & 0x80) >> 1));
909 g.setNumberMoves(((int) hdr[12]) & 0xff);
910 }
911 /**
912 * Extract year game was played.
913 *
914 * @param hdr <CODE>byte[]</CODE> containing the decoded header bytes read
915 * from the database
916 * @return <CODE>int</CODE> containing the year that the game was played,
917 * or <CODE>0</CODE> in case no year information was present in the header
918 */
919 protected int extractYear(byte[] hdr) {
920 int y = ((int) hdr[0]) & 0xff;
921 if (y == 127) {
922 return 0;
923 } else {
924 return (y + 1900);
925 }
926 }
927 /**
928 * Extract ELO of white player.
929 *
930 * @param hdr <CODE>byte[]</CODE> containing the decoded header bytes read
931 * from the database
932 * @return <CODE>int</CODE> containing the ELO points of white or
933 * <CODE>0</CODE> in case no such information was present
934 */
935 protected int extractWhiteELO(byte[] hdr) {
936 if (hdr[8] != 0) {
937 return (1600 + 5 * (((int) hdr[8]) & 0xff));
938 } else {
939 return 0;
940 }
941 }
942 /**
943 * Extract ELO of black player.
944 *
945 * @param hdr <CODE>byte[]</CODE> containing the decoded header bytes read
946 * from the database
947 * @return <CODE>int</CODE> containing the ELO points of black or
948 * <CODE>0</CODE> in case no such information was present
949 */
950 protected int extractBlackELO(byte[] hdr) {
951 if (hdr[9] != 0) {
952 return (1600 + 5 * (((int) hdr[9]) & 0xff));
953 } else {
954 return 0;
955 }
956 }
957 /**
958 * Extract ECO code for game.
959 *
960 * @param hdr <CODE>byte[]</CODE> containing the decoded header bytes read
961 * from the database
962 * @return <CODE>String</CODE> containing the ECO code for the game
963 */
964 protected String extractECO(byte[] hdr) {
965 int e = ((((int) hdr[10]) & 0x3e) >> 1) |
966 ((((int) hdr[4]) & 0xc0) >> 1) |
967 ((((int) hdr[5]) & 0xc0) << 1);
968 StringBuffer out = new StringBuffer();
969 out.append((char) ((int) 'A' + ((e - 1) / 100)));
970 out.append((e - 1) %100);
971 out.append('.');
972 out.append((((int) hdr[11]) & 0x3f) |
973 ((((int) hdr[11]) & 0x80) >> 1));
974 return out.toString();
975 }
976 /**
977 * Process textual header information such as names of players, tournament
978 * etc.
979 *
980 * @param txt <CODE>byte[]</CODE> containing the decoded game header
981 * text bytes
982 * @param plen <CODE>int</CODE> containing the length of the players
983 * section in <CODE>txt</CODE>
984 * @param g <CODE>Game</CODE> to store information in
985 */
986 protected void parseHeaderText(byte[] txt, int plen, Game g) {
987 //
988 // Do actual parsing first
989 //
990 String[] flds = parseHeaderText(txt, plen);
991 //
992 // Now stuff all non-null components in the Game object
993 //
994 if (flds[0] != null) {
995 g.setRound(flds[0]);
996 }
997 if (flds[1] != null) {
998 g.setAnnotator(flds[1]);
999 }
1000 if (flds[2] != null) {
1001 g.setWhitePlayer(flds[2]);
1002 }
1003 if (flds[3] != null) {
1004 g.setBlackPlayer(flds[3]);
1005 }
1006 if (flds[4] != null) {
1007 g.setTournament(flds[4]);
1008 }
1009 }
1010 /**
1011 * Process textual header information such as names of players, tournament
1012 * etc.
1013 *
1014 * @param txt <CODE>byte[]</CODE> containing the decoded game header
1015 * text bytes
1016 * @param plen <CODE>int</CODE> containing the length of the players
1017 * section in <CODE>txt</CODE>
1018 * @param g <CODE>Game</CODE> to store information in
1019 * @return <CODE>String[]</CODE> containing the different game info
1020 * strings. The returned items are like this:
1021 * <p>
1022 * <table>
1023 * <tr><th>Subscript</th><th>Contents</th></tr>
1024 * <tr><td>0</td><td>Round</td></tr>
1025 * <tr><td>1</td><td>Annotator</td></tr>
1026 * <tr><td>2</td><td>Name white player</td></tr>
1027 * <tr><td>3</td><td>Name black player</td></tr>
1028 * <tr><td>4</td><td>Tournament</td></tr>
1029 * </table>
1030 * <p>
1031 * Whenever a certain item is not present in the game, its entry is
1032 * set to <CODE>null</CODE>
1033 */
1034 protected String[] parseHeaderText(byte[] txt, int plen) {
1035 String[] out = new String[5];
1036 //
1037 // The order in which the different components are retrieved does
1038 // matter, so don't start shuffling the code around ;-)
1039 //
1040 out[0] = extractRound(txt);
1041 out[1] = extractAnnotator(txt, plen);
1042 //
1043 // Try to find both players names
1044 //
1045 int l = 0; // Destination index during trimming, length afterwards
1046 int i = 0; // Source index during trimming
1047 int h1 = -1; // Location first '-'
1048 int h2 = -1; // Location second '-'
1049 int cm = -1; // Location first ','
1050 for (i = 0; i < plen; ++i) {
1051 if (Character.isWhitespace((char) txt[i])) {
1052 if ((i == l) || !Character.isWhitespace((char) txt[l])) {
1053 txt[l] = txt[i];
1054 ++l; // Do *NOT* use autoincrement in previous move !!!!
1055 }
1056 } else {
1057 if (((char) txt[i] == '-') && (h1 < 0)) {
1058 h1 = l;
1059 } else if (((char) txt[i] == '-') && (h2 < 0)) {
1060 h2 = l;
1061 } else if (((char) txt[i] == ',') && (cm < 0)) {
1062 cm = l;
1063 }
1064 txt[l] = txt[i];
1065 ++l; // Do *NOT* use autoincrement in previous move !!!!
1066 }
1067 }
1068 if ((h1 > 0) && (h2 > 0) && (cm > h1)) {
1069 h1 = h2;
1070 }
1071 if (h1 > 0) {
1072 out[2] = (CBFUtil.bytesToString(txt, 0, h1).trim());
1073 if (h1 < l - 2) {
1074 out[3] = (CBFUtil.bytesToString(txt, h1 + 1,
1075 l - h1 - 1).trim());
1076 }
1077 } else {
1078 out[2] = (CBFUtil.bytesToString(txt, 0, l).trim());
1079 out[3] = null;
1080 }
1081 //
1082 // Tournament info is whatever remains in `source' part
1083 //
1084 l = plen;
1085 int last_non_space = plen - 1;
1086 for (i = plen; i < txt.length; ++i) { // First trim source string
1087 if (Character.isWhitespace((char) txt[i])) {
1088 if ((i == l) || !Character.isWhitespace((char) txt[l])) {
1089 txt[l] = txt[i];
1090 ++l; // Do *NOT* use autoincrement in previous move !!!!
1091 }
1092 } else {
1093 last_non_space = l;
1094 txt[l] = txt[i];
1095 ++l; // Do *NOT* use autoincrement in previous move !!!!
1096 }
1097 }
1098 if(last_non_space >= plen) {
1099 out[4] = CBFUtil.bytesToString(txt, plen,
1100 last_non_space - plen + 1);
1101 } else {
1102 out[4] = null;
1103 }
1104 return out;
1105 }
1106 /**
1107 * Extract round information from specified string.
1108 * <p>
1109 * Round information is expected to be between parenthesis or brackets,
1110 * but a slash, followed by a left parenthesis is not a valid round
1111 * prefix.
1112 * <p>
1113 * On return, the input parameter may be changed in that the bytes that
1114 * contained the round information are set to spaces.
1115 *
1116 * @param p <CODE>byte[]</CODE> containing the text to search for round
1117 * information
1118 * @return <CODE>String</CODE> containing the round information or
1119 * <CODE>null</CODE> in case no round information was found.
1120 */
1121 protected String extractRound(byte[] p) {
1122 String out = null;
1123 int i = 0;
1124 int a = -1;
1125 char mark = '\0';
1126
1127 while (i < p.length) {
1128 if (((p[i] == '(') && ((i == 0) || (p[i - 1] != '/'))) ||
1129 (p[i] == '[')) {
1130 mark = ((p[i] == '(') ? ')' : ']');
1131 a = i++;
1132 if ((p[i] == 'm') && (p[i + 1] == '/')) {
1133 //
1134 // Skip `m/'
1135 //
1136 i += 2;
1137 }
1138 while ((i < p.length) && (p[i] != mark)) {
1139 if (Character.isDigit((char) p[i]) || (p[i] == '.')) {
1140 ++i;
1141 } else {
1142 break;
1143 }
1144 }
1145 if ((i < p.length) && (p[i] == mark)) {
1146 int l = i - a + 1;
1147 if (l > 2) {
1148 out = CBFUtil.bytesToString(p, a + 1, l - 2);
1149 for (int j = 0; j < l; ++j) {
1150 p[j + a] = (byte) ' ';
1151 }
1152 break;
1153 }
1154 }
1155 }
1156 ++i;
1157 }
1158 return out;
1159 }
1160 /**
1161 * Extract annotator information from specified string.
1162 * <p>
1163 * Annotator information is expected to be between brackets.
1164 * <p>
1165 * On return, the input parameter may be changed.
1166 *
1167 * @param p <CODE>byte[]</CODE> containing the text to search for annotator
1168 * information
1169 * @param ofs <CODE>int</CODE> containing the byte offset from where to
1170 * start looking for the annotator.
1171 * @return <CODE>String</CODE> containing the annotator information or
1172 * <CODE>null</CODE> in case no annotator information was found.
1173 */
1174 protected String extractAnnotator(byte[] p, int ofs) {
1175 String out = null;
1176 int i = ofs;
1177 int a = -1;
1178
1179 while (i < p.length) {
1180 if (p[i] == '[') {
1181 a = i++;
1182 while ((i < p.length) && (p[i] != ']')) {
1183 ++i;
1184 }
1185 if ((i < p.length) && (p[i] == ']')) {
1186 int l = i - a + 1;
1187 if (l > 2) {
1188 out = CBFUtil.bytesToString(p, a + 1, l - 2);
1189 for (int j = 0; j < l; ++j) {
1190 p[j + a] = (byte) ' ';
1191 }
1192 break;
1193 }
1194 }
1195 }
1196 ++i;
1197 }
1198 return out;
1199 }
1200 /**
1201 * Process contents of moves and comments byte arrays and convert them
1202 * to the proper format as required by this package.
1203 *
1204 * @param g <CODE>Game</CODE> to which the moves belong. The properties
1205 * of this object <strong>will</strong> be changed by this method.
1206 * @param mvs <CODE>byte[]</CODE> containing the game moves
1207 * @param notes <CODE>Vector</CODE> containing the move comments. Note that
1208 * this <CODE>Vector</CODE> is gradually emptied until is is either empty
1209 * or contains only the game level annotation entry.
1210 * @exception java.lang.Exception if an error occured during the operation.
1211 */
1212 protected void processMoves(Game g, byte[] mvs, Vector notes)
1213 throws Exception {
1214 //
1215 // Unobfuscate moves ...
1216 //
1217 int l = mvs.length + 1;
1218 int mask = l * 49;
1219 for (int i = l - 2; i > 0; --i) {
1220 mvs[i] ^= mask;
1221 mask *= 7;
1222 }
1223 //
1224 // Process moves and variations.
1225 //
1226 // Note:
1227 //
1228 // The original C-code (see comments at class-level) use a
1229 // recursive scheme for wading through the variations tree. Normally,
1230 // this would be a naturally method for processing the recursive
1231 // logical structure in the game movelists. However, since Java
1232 // discourages so called `side-effects' (i.e. where input parameters
1233 // are modified by called functions) and the fact that I dislike using
1234 // global data in multithreaded environment (Java), I decided to
1235 // flatten out the processing of the moves and their comments (with
1236 // the help of a handfull of stack objects)
1237 //
1238 Board brd = new Board(g.getInitialPosition());
1239 int varlevel = 0;
1240 int halfmove = brd.getNextHalfMoveNumber();
1241 int m = 0; // Index next move in `mvs'
1242 Stack board_stack = new Stack(); // Contains saved board setups
1243 Stack halfmove_stack = new Stack(); // Contains saved halfmove nrs
1244 Stack moves_stack = new Stack(); // Contains saved movelists
1245 boolean has_comment = false;
1246 Vector movelist = new Vector(); // Movelist of `current' variation
1247 Board prevbrd = null;
1248 Stack prvboard_stack = new Stack(); // Boards before start vars.
1249 while (m < mvs.length) {
1250 int move = (int) mvs[m++] & 0xff;
1251 if (move == END_OF_VARIATION) {
1252 //
1253 // End of variation reached
1254 //
1255 if (varlevel == 0) {
1256 throw new Exception("Spurious end-variation-marker: " +
1257 "halfmove=" + halfmove);
1258 }
1259 //
1260 // No special handling required on move-level
1261 //
1262 --varlevel;
1263 halfmove = ((Integer) halfmove_stack.pop()).intValue();
1264 brd = (Board) board_stack.pop();
1265 prevbrd = (Board) prvboard_stack.pop();
1266 Vector v = (Vector) moves_stack.pop();
1267 //
1268 // Add current movelist (= list of moves in variation) to
1269 // variations list of last element in vector `v' (which
1270 // contains the moves in the higher level variation)
1271 //
1272 Move mv = (Move) v.elementAt(v.size() - 1);
1273 mv.addVariation(movelist);
1274 movelist = v; // Continue with higher-level variation
1275 } else if (move == START_OF_VARIATION) {
1276 //
1277 // Detected start of variation
1278 //
1279 // No special handling required on move-level
1280 //
1281 ++varlevel;
1282 halfmove_stack.push(new Integer(halfmove));
1283 board_stack.push(brd); // Pos in main variation
1284 prvboard_stack.push(prevbrd);
1285 moves_stack.push(movelist);
1286 movelist = new Vector();
1287 brd = new Board(prevbrd); // Undo move from main variation
1288 --halfmove;
1289 } else {
1290 //
1291 // Ordinary move
1292 //
1293 has_comment = (((move & 0x80) != 0)&& (notes.size() > 0));
1294 move = (move &0x7f) - 1; // Zero based move number
1295 //
1296 // HACK: all non-zero colors will be considered black
1297 //
1298 int c = (halfmove - 1) % 2;
1299 Vector v = brd.generateMoveList(c,
1300 getMoveGenerator());
1301 if (move >= v.size()) {
1302 throw new Exception("Houston, we have a problem: move " +
1303 move + " does not fit in list of " + v.size() +
1304 " valid moves");
1305 }
1306 Move newmove = (Move) v.elementAt(move);
1307 if (has_comment && (notes.size() > 0)) {
1308 //
1309 // Grab all sorts of annotation and move evaluations
1310 //
1311 CBFAnnotation an = (CBFAnnotation) notes.elementAt(0);
1312 an.decorateMove(newmove);
1313 notes.removeElementAt(0);
1314 }
1315 movelist.addElement(newmove);
1316 prevbrd = new Board(brd); // Variations start from this pos.
1317 brd.doMove(newmove);
1318 ++halfmove;
1319 }
1320 }
1321 //
1322 // By now `movelist' contains the moves of the main variation (that
1323 // is: the moves actually played in the game)
1324 //
1325 g.setMoves(movelist);
1326 //
1327 // Check for game-level annotations.
1328 //
1329 if (notes.size() > 0) {
1330 CBFAnnotation an = (CBFAnnotation) notes.elementAt(0);
1331 g.setGameAnnotation(an.getAnnotation());
1332 }
1333 }
1334 /**
1335 * Build players info part of a game to store in the database.
1336 *
1337 * @param g <CODE>Game</CODE> to grab information from
1338 * @return <CODE>byte[]</CODE> containing the players information
1339 */
1340 protected byte[] buildPlayersInfo(Game g) {
1341 String s = g.getWhitePlayer() + "-" + g.getBlackPlayer();
1342 return CBFUtil.stringToBytes(s);
1343 }
1344 /**
1345 * Build game source info (that is: tournament, round and annotator).
1346 *
1347 * @param g <CODE>Game</CODE> to grab information from
1348 * @return <CODE>byte[]</CODE> containing the source information.
1349 */
1350 protected byte[] buildSourceInfo(Game g) {
1351 String s = "";
1352 String t;
1353 if ((t = g.getTournament()).length() > 0) {
1354 s = t;
1355 }
1356 if ((t = g.getRound()).length() > 0) {
1357 s += "(" + t + ")";
1358 }
1359 if ((t = g.getAnnotataor()).length() > 0) {
1360 s += "[" + t + "]";
1361 }
1362 return CBFUtil.stringToBytes(s);
1363 }
1364 /**
1365 * Build move list for game.
1366 *
1367 * @param bos <CODE>ByteArrayOutputStream</CODE> to write moves to
1368 * @param cos <CODE>ByteArrayOutputStream</CODE> to write comments to
1369 * @param mvs <CODE>Vector</CODE> containing the variation to process
1370 * @param brd <CODE>Board</CODE> with setup at beginning of the
1371 * variation
1372 * @param halfmove <CODE>int</CODE> containing the number of the halfmove
1373 * that is at the start of the variation (first move of the game is
1374 * halfmove one)
1375 */
1376 protected void appendMoves(ByteArrayOutputStream bos,
1377 ByteArrayOutputStream cos, Vector mvs,
1378 Board brd, int halfmove) {
1379 Board b = new Board(brd); // Operate on copy of board
1380 for (Enumeration e = mvs.elements(); e.hasMoreElements(); ) {
1381 Object oo = e.nextElement();
1382 Move m = (Move) oo;
1383 //
1384 // HACK: all non-zero colors will be considered black
1385 //
1386 int c = (halfmove - 1) % 2;
1387 Vector v = b.generateMoveList(c, getMoveGenerator());
1388 int mvnr = 0;
1389 for (Enumeration en = v.elements(); en.hasMoreElements(); ) {
1390 Move mv = (Move) en.nextElement();
1391 if (m.equals(mv)) {
1392 break;
1393 }
1394 ++mvnr;
1395 }
1396 if (mvnr > v.size()) {
1397 System.err.println("Woopsa: move " + m +
1398 " seems fishy in this position:\n" + b +
1399 "\n with " + ((c == 0) ? "white" : "black") + " to move");
1400 System.err.flush();
1401 System.exit(1);
1402 }
1403 ++mvnr; // First move has value 1 in database
1404 if (m.isAnnotated()) {
1405 //
1406 // Update comments output.
1407 //
1408 mvnr |= 0x80;
1409 CBFAnnotation.annotationToDBF(cos, m);
1410 }
1411 bos.write(mvnr);
1412 //
1413 // Done move, check for variations.
1414 //
1415 if (m.hasVariations()) {
1416 Vector vars = m.getVariations();
1417 bos.write(START_OF_VARIATION);
1418 for (Enumeration ee = vars.elements();
1419 ee.hasMoreElements(); ) {
1420 Vector branch = (Vector) ee.nextElement();
1421 appendMoves(bos, cos, branch, b, halfmove);
1422 }
1423 bos.write(END_OF_VARIATION);
1424 }
1425 //
1426 // All done for this move: execute on board and try next
1427 //
1428 try {
1429 b.doMove(m);
1430 } catch (Exception weirdo) {
1431 //
1432 // Typical case of `can never happen', so ignore ;-)
1433 //
1434 }
1435 ++halfmove;
1436 }
1437 }
1438 /**
1439 * Local helper routine to read and convert 32-bit integer in intel-CPU
1440 * format to Java representation.
1441 * <p>
1442 * The intel format stores a 32-bit 0x12345678 as bytes 0x34, 0x12,
1443 * 0x78, 0x56.
1444 *
1445 * @param f <CODE>RandomAccessFile</CODE> to read from
1446 * @param o <CODE>long</CODE> offset to read from
1447 * @return <CODE>int</CODE> containing the java 32-bit integer value
1448 * @exception IOException if an I/O error occurs
1449 */
1450 protected int slurpInt(RandomAccessFile f, long o) throws IOException {
1451 f.seek(o);
1452 int b4 = (int) f.readByte() & 0xff;
1453 int b3 = (int) f.readByte() & 0xff;
1454 int b2 = (int) f.readByte() & 0xff;
1455 int b1 = (int) f.readByte() & 0xff;
1456 int i = (((b4 << 8) | b3) << 8 | b2) << 8 | b1;
1457 return i;
1458 }
1459 /**
1460 * Local helper routine to read and convert 16-bit integer in intel-CPU
1461 * format to Java representation.
1462 * <p>
1463 * The intel format stores a 16-bit 0x1234 as bytes 0x34, 0x12.
1464 *
1465 * @param f <CODE>RandomAccessFile</CODE> to read from
1466 * @param o <CODE>long</CODE> offset to read from
1467 * @return <CODE>int</CODE> containing the java 16-bit integer value
1468 * @exception IOException if an I/O error occurs
1469 */
1470 protected int slurpShort(RandomAccessFile f, long o) throws IOException {
1471 f.seek(o);
1472 int b2 = (int) f.readByte() & 0xff;
1473 int b1 = (int) f.readByte() & 0xff;
1474 int i = b2 << 8 | b1;
1475 return i;
1476 }
1477 /**
1478 * Local helper routine to read and convert unsigned 8-bit integer (i.e.
1479 * one byte)
1480 *
1481 * @param f <CODE>RandomAccessFile</CODE> to read from
1482 * @param o <CODE>long</CODE> offset to read from
1483 * @return <CODE>int</CODE> containing the java 8-bit integer value
1484 * @exception IOException if an I/O error occurs
1485 */
1486 protected int slurpByte(RandomAccessFile f, long o) throws IOException {
1487 f.seek(o);
1488 return (int) f.readByte() & 0xff;
1489 }
1490 /**
1491 * Local helper method for reading a specified amount of bytes from a file,
1492 * starting at the specified file offset.
1493 * <p>
1494 * Note that this method will block until all requested bytes are read
1495 * (or until an end-of-file or an exception is detected)
1496 *
1497 * @param f <CODE>RandomAccessFile</CODE> to read from
1498 * @param o <CODE>long</CODE> offset to read from
1499 * @param len <CODE>int</CODE> number of bytes to read.
1500 * @return <CODE>byte[]</CODE> of length <CODE>len</CODE> bytes containing
1501 * the requested data.
1502 * @exception IOException if an I/O error occurs
1503 */
1504 protected byte[] slurp(RandomAccessFile f, long o, int len)
1505 throws IOException {
1506 byte[] bar = new byte[len];
1507 f.seek(o);
1508 f.readFully(bar);
1509 return bar;
1510 }
1511 /**
1512 * Local helper routine to convert 32-bit integer from Java representation
1513 * to intel-CPU format.
1514 * <p>
1515 * The intel format stores a 32-bit 0x12345678 as bytes 0x34, 0x12,
1516 * 0x78, 0x56.
1517 *
1518 * @param f <CODE>RandomAccessFile</CODE> to read from
1519 * @param o <CODE>long</CODE> offset to read from
1520 * @param i <CODE>int</CODE> to write
1521 * @exception IOException if an I/O error occurs
1522 */
1523 protected void barfInt(RandomAccessFile f, long o, int i)
1524 throws IOException {
1525 int b1 = i & 0xff;
1526 i >>= 8;
1527 int b2 = i & 0xff;
1528 i >>= 8;
1529 int b3 = i & 0xff;
1530 i >>= 8;
1531 int b4 = i & 0xff;
1532 f.seek(o);
1533 f.write(b4);
1534 f.write(b3);
1535 f.write(b2);
1536 f.write(b1);
1537 }
1538 /**
1539 * Write out a 16-bit integer to file at given position and in intel-CPU
1540 * format.
1541 * <p>
1542 * The intel format stores a 16-bit 0x1234 as bytes 0x34, 0x12.
1543 *
1544 * @param f <CODE>RandomAccessFile</CODE> to write to
1545 * @param o <CODE>long</CODE> offset to write to
1546 * @param s <CODE>int</CODE> 16-bit integer to write
1547 * @exception IOException if an I/O error occurs
1548 */
1549 protected void barfShort(RandomAccessFile f, long o, int s)
1550 throws IOException {
1551 int b2 = (s >> 8) & 0xff;
1552 int b1 = s & 0xff;
1553 int i = b2 << 8 | b1;
1554 f.seek(o);
1555 f.write(b2);
1556 f.write(b1);
1557 }
1558 /**
1559 * Write out one single byte to file at given position.
1560 *
1561 * @param f <CODE>RandomAccessFile</CODE> to write to
1562 * @param o <CODE>long</CODE> offset to write to
1563 * @param b <CODE>byte</CODE> to write
1564 * @exception IOException if an I/O error occurs
1565 */
1566 protected void barfByte(RandomAccessFile f, long o, byte b)
1567 throws IOException {
1568 f.seek(o);
1569 f.write(b);
1570 }
1571 /**
1572 * Write specified byte array to file at given position.
1573 *
1574 * @param f <CODE>RandomAccessFile</CODE> to write to
1575 * @param o <CODE>long</CODE> offset to write to
1576 * @param bar <CODE>byte[]</CODE> to write
1577 * @param ofs <CODE>int</CODE> offset in <CODE>bar</CODE> from where to
1578 * write out its contents
1579 * @param len <CODE>int</CODE> number of bytes to write
1580 * @exception IOException if an I/O error occurs
1581 */
1582 protected void barf(RandomAccessFile f, long o, byte[] bar, int ofs,
1583 int len) throws IOException {
1584 f.seek(o);
1585 f.write(bar, ofs, len);
1586 }
1587
1588 // Do not add or change code below this line.
1589 public static final String __version = "$Revision: 1.3 $";
1590 }