// Game of Fanorona
// David Eppstein, UC Irvine, 28 Sep 1997
//
// Endgame database (application version only!)
//
// Usage:
//
// Call endgameDatabase.lookup(Board b)) to look up an individual board.
// It will return true if the database is set up and the board is there,
// with the correct value set in the board's evaluation.  Note that the board
// is assumed (but not checked) to be not midCapture().
//
// Call endgameDatabase.search(Board b) to find the best move for a board.
// The board may be midCapture(). Again, it returns true if successful,
// false if a full alpha-beta search needs to be performed.

import java.io.*;

public class EndgameDatabase {
	// The database contents
	byte db[];
	boolean ready = false;
	
	// Conditional compilation for db construction code
	static final boolean CREATE_DATABASE = false;
	static final boolean SANITY_CHECKS = false;
	
	// Convert contents to and from compact single-byte representation.
	// Assumes winning evals are always an odd multiple of PLY_DECREMENT
	// and losing evals are always an even multiple.
	final byte evalByte(int eval) {
		if (eval == 0) return 0;
		int x = (eval < 0? -eval : eval);
		int base, ply;
		if (x <= Board.WON_POSITION) {
			base = 64;
			ply = Board.WON_POSITION - x;
		} else {
			base = 128;
			ply = 2*Board.WON_POSITION - x;
		}
		if (ply <= 0) {
			if (SANITY_CHECKS) System.out.println("Non-positive ply! " + ply + " from eval " + eval);
			ply = 1;
		} else if (ply >= 64) {
			if (SANITY_CHECKS) System.out.println("Huge ply! " + ply + " from eval " + eval);
			ply = 63;
		} int b = base - ply;
		return (byte)(eval < 0? -b : b);
	}
	final int eval(byte b) {
		if (b == 0) return 0;
		int x = (b < 0? -b : b);
		int base, ply;
		if (x <= 64) {
			base = Board.WON_POSITION;
			ply = 64-x;
		} else {
			base = 2 * Board.WON_POSITION;
			ply = 128-x;
		}
		x = base - ply;
		return (b < 0? -x : x);
	}
	
	// First step in database lookup: check if few enough pieces that it will be there
	final boolean smallEnough(long pieces) {
		pieces &= Bits.ON_BOARD & pieces - 1;
		return (pieces & (pieces - 1)) == 0;
	}
	
	// Second step in database lookup: convert bitboard to 12-bit representation
	final short compact(long pieces) {
		pieces &= Bits.ON_BOARD;
		long y = pieces - 1;
		int x = Bits.count(pieces ^ y);
		pieces &= y;
		return (short) ((x << 6) + Bits.count(pieces ^ (pieces - 1)));
	}
	
	// Third step in database lookup: convert 12-bit to even shorter representation
	// Reverse conversion can be done by lookup in array pieces[].
	short shortReps[];
	short numShortReps;
	long pieces[];
	short shortRep(long pieces) { return shortReps[compact(pieces)]; }
	final void setupShortReps() {
		int i,j,k,l;
		numShortReps = 0;
		shortReps = new short[1<<12];
		for (i = 0; i < 5; i++) for (j = 0; j < 9; j++)
			for (k = 0; k <= i; k++) for (l = 0; l < 9; l++) {
				if (k == i && l > j) continue;
				short c = compact(Bits.at(i,j) | Bits.at(k,l));
				if (SANITY_CHECKS && shortReps[c] != 0)
					System.out.println("Short rep already nonzero!");
				shortReps[c] = numShortReps++;
			}
		if (SANITY_CHECKS && numShortReps != 45*23)
			System.out.println("Number of short reps: " + numShortReps);
		if (CREATE_DATABASE) {
			pieces = new long[numShortReps];
			for (i = 0; i < 5; i++) for (j = 0; j < 9; j++)
				for (k = 0; k <= i; k++) for (l = 0; l < 9; l++) {
					if (k == i && l > j) continue;
					long p = Bits.at(i,j) | Bits.at(k,l);
					pieces[shortReps[compact(p)]] = p;
				}
		}
	}
	
	// Fourth step in database lookup: action of symmetry
	//
	// The symmetry group is Z2 * Z2 which we represent as a two-bit number,
	// one bit reversing rows, the other reversing columns.
	// Fortunately, we don't need to distinguish group elements from their inverses.
	//
	short syms[][];	// indexed by symmetry(0-3), shortRep
	byte minSym[];		// which symmetry produces the smallest shortRep?
	short symReps[];	// conversion table to symmetric representations
	short shortRepForSym[];	// and vice versa
	short numSymReps;
	final void setupSyms() {
		syms = new short[4][];
		for (int i = 0; i < 4; i++) syms[i] = new short[numShortReps];
		minSym = new byte[numShortReps];
		symReps = new short[numShortReps];
		int i,j,k,l;
		for (i = 0; i < 5; i++) for (j = 0; j < 9; j++)
			for (k = 0; k <= i; k++) for (l = 0; l < 9; l++) {
				if (k == i && l > j) continue;
				short s = shortReps[compact(Bits.at(i,j) | Bits.at(k,l))];
				syms[0][s] = s;
				syms[1][s] = shortReps[compact(Bits.at(4-i,j) | Bits.at(4-k,l))];
				syms[2][s] = shortReps[compact(Bits.at(i,8-j) | Bits.at(k,8-l))];
				syms[3][s] = shortReps[compact(Bits.at(4-i,8-j) | Bits.at(4-k,8-l))];
				if (minSym[s] == 0) {
					symReps[s] = numSymReps++;
					for (byte r = 1; r < 4; r++) {
						if (syms[r][s] != s) minSym[syms[r][s]] = r;
						symReps[syms[r][s]] = symReps[s];
					}
				}
			}
		if (CREATE_DATABASE) {
			shortRepForSym = new short[numSymReps];
			for (short s = 0; s < numShortReps; s++)
				if (minSym[s] == 0) {
					if (SANITY_CHECKS && shortRepForSym[symReps[s]] != 0)
						System.out.println("shortRepForSym already nonzero!");
					shortRepForSym[symReps[s]] = s;
				}
		}
	}
	
	// Perform lookup
	final boolean lookup(Board b) {
		short myPieceRep = shortReps[compact(b.myPieces)];
		short oppPieceRep = shortReps[compact(b.opponentPieces)];
		byte sym = minSym[myPieceRep];
		int index = symReps[myPieceRep] * numShortReps + syms[sym][oppPieceRep];
		b.evaluation = eval(db[index]);
		return true;
	}
	
	// Do one-ply (plus multiple-capture) search, return results in evaluation and bestMove
	// To avoid complete aimlessness, if we're using this to play (rather than to construct
	// the database itself) we call evaluate on any drawn position.
	//
	final boolean search(Board b) {
		if (!smallEnough(b.myPieces) || !smallEnough(b.opponentPieces)) return false;
		if (SANITY_CHECKS && (b.myPieces & Bits.ON_BOARD) == 0)
			System.out.println("No pieces in search!");
		b.setMoveGenerator();
		b.bestMove = -1L;
		b.evaluation = -Integer.MAX_VALUE;
		long move;
		while ((move = b.moveGenerator.nextElement()) >= 0) {
			b.setChild(move);
			int childEval;
			if ((b.child.opponentPieces & Bits.ON_BOARD) == 0) {	// wipe out
				childEval =
					Bits.count(b.myPieces & Bits.ON_BOARD) * Board.WON_POSITION;
			} else if (b.child.myPieces < 0)	{			// if (child.midCapture())
				search(b.child);								// search recursively
				childEval = b.child.evaluation;
			} else {												// change sides, not all captured
				lookup(b.child);								// find in database
				childEval = -b.child.evaluation;			// negate to get score from our pov

				if (ready && childEval == 0) {			// using search to choose endgame move?
					ready = false;								// disable recursive database lookup
					if (Eval.evaluate(b.child))
						childEval = -b.child.evaluation;	// do positional evaluation of drawn pos
					ready = true;								// turn database back on
				}
			}
			if (childEval > 0) childEval -= Board.PLY_DECREMENT;
			else if (childEval < 0) childEval += Board.PLY_DECREMENT;
			if (childEval > b.evaluation) {
				b.evaluation = childEval;
				b.bestMove = move;
				if (ready) b.setPrincipalVariation();
			}
		}
		if (SANITY_CHECKS && b.evaluation == -Integer.MAX_VALUE)
			System.out.println("No moves found!");
		return true;
	}
	
	// Compute database!
	//
	// There are more sophisticated ways of doing this, but it's a little complicated
	// (involving generating moves backwards rather than forwards) and made even more
	// complex by the fact that our eval counts material at the win not just plys to win.
	// So, the algorithm we use is straightforward:
	//
	//	do {
	//		call search() on all positions in the db
	//		store changed evaluations back into the db
	// } while (!changed > 0)
	//
	final void setupDatabase() {
		if (SANITY_CHECKS) {
			for (short symRep = 0; symRep < numSymReps; symRep++) {
				if (symRep != symReps[shortRepForSym[symRep]])
					System.out.println("Non-inverse! symReps[shortRepForSym[" + symRep +
						"]] = " + symReps[shortRepForSym[symRep]]);
				long p = pieces[shortRepForSym[symRep]];
				if (minSym[shortRepForSym[symRep]] != 0) System.out.println("Asymmetric rep!");
				if (shortRepForSym[symRep] != shortReps[compact(p)]) System.out.println("Wrong shortrep!");
			}
			for (short shortRep = 0; shortRep < numShortReps; shortRep++) {
				if (pieces[shortRep] == 0) System.out.println("No pieces!");
				if (shortRep != syms[minSym[shortRep]][shortRepForSym[symReps[shortRep]]])
					System.out.println("Assymetric short rep!");
			}
		}
		db = new byte[numSymReps * numShortReps];
		System.out.println("Creating endgame database... " + numSymReps * numShortReps + " entries");
		int iterations = 0;
		Board b = new Board();
		int numChanges;
		do {
			numChanges = 0;
			for (short symRep = 0; symRep < numSymReps; symRep++) {
				b.myPieces = pieces[shortRepForSym[symRep]];
				for (short shortRep = 0; shortRep < numShortReps; shortRep++) {
					b.opponentPieces = pieces[shortRep];
					if ((b.myPieces & b.opponentPieces) != 0) continue;
					search(b);
					byte newEval = evalByte(b.evaluation);
					int index = symRep * numShortReps + shortRep;
					if (db[index] != newEval) {
						if (SANITY_CHECKS) {
							if (db[index] > 0 && newEval < db[index])
								System.out.println("Eval got worse after win found! " +
									db[index] + "=>" + newEval);
							else if (db[index] < 0 && newEval > db[index])
								System.out.println("Eval got better after loss found! " +
									db[index] + "=>" + newEval);
						}
						db[index] = newEval;
						numChanges++;
					}
				}
			}
			System.out.println("Iteration " + (++iterations) + ": " + numChanges + " updated evals");
		} while (numChanges > 0);
		ready = true;
	}
	
	// set up the whole shebang
	EndgameDatabase()
	{
		setupShortReps();
		setupSyms();
		try {
			String filename = System.getProperty( "endgame.database.filename" );
			if (filename != null) {
				InputStream stream = new FileInputStream(filename);
				db = new byte[numSymReps * numShortReps];
				stream.read(db);
				stream.close();
				ready = true;
			}
		} catch (IOException e) {
			System.out.println("Unable to read endgame database: " + e.getMessage());
			if (CREATE_DATABASE) {
				setupDatabase();
				System.out.println("Writing database to file");
				try {
					OutputStream stream = new FileOutputStream("endgame.db");
					stream.write(db);
					stream.close();
				} catch (IOException ioe) {
					System.out.println("Write failed: " + ioe.getMessage());
				}
			} else {
				db = null;
				ready = false;
			}
		}
	}
}
