/** (C) Game Page Network, Inc., Confidential, All Rights Reserved */
// Game.java
// --paul@gamepage.net, 31jul97

import java.io.*; // for main() standalone testing (see below)
import java.util.*;
import java.math.*;

/** GPN Game class for Xiang Qi (Chinese Chess) implementation.
 * /// TODO: undo, autosave/resume, email
 */
public class Game {
  // Piece notation is standard with WXL, WXF and AXF.
  // Cf http://xiangqi.com/notation.html.
  private final static String VALID_PIECES = "KAEHRCPkaehrcp ";
  
  // Instance fields:
  private Table table;
  private String name;
  private char[][] board = new char[10][9];
  private int posK, posk;	// remember where the kings live
  private Member player1, player2;
  private String player1name, player2name;
  private String player1info, player2info;
  private Member turnNow;
  public char gameState = 'b';	// Begin else Run else End
  public boolean practice;
  private Member draw;		// Anyone request a draw yet?
  private boolean validate;	// Validate moves if unregistered servers
  private boolean debug;	// whether to display debug output to stdout
  private Vector history = new Vector(100,50); // move list and results

  public String toString()      { return name+" "+player1+" "+player2; }
  public boolean isActive()     { return gameState=='r'; }
  public Member whoseTurn()     { return turnNow; }
  public boolean myTurn(Member member) {
    return practice || member==turnNow;
  }
  public boolean isLead(Member m) { return m == player1; }

  // -----------------------------------------------------------------
  // Game timers:
  private long gameTimer;	// number of minutes we started with
  private long moveTimer;	// FYI client move timer setting
  private long p1seconds,p2seconds; // seconds left for player for game
  private long p1secPrev,p2secPrev; // previous seconds left
  private long timeStamp;	// timestamp of last move

  /** Set the amount of time each player will have for the game. */
  public void setTimer(int gameTimer, int moveTimer) {
    this.gameTimer = gameTimer;
    this.moveTimer = moveTimer;
    p1seconds = p2seconds = gameTimer*60;
    p1secPrev = p2secPrev = gameTimer*60;
    timeStamp = System.currentTimeMillis();
  }
  /** Report GAMEMINUTES MOVEMINUTES P1SECONDS P2SECONDS for new observer */
  public String getTimers() {
    if (gameState=='r')
      updateTimers();
    return gameTimer+" "+moveTimer+" "+p1seconds+" "+p2seconds;
  }
  public void updateTimers() {
    long was = timeStamp;
    long now = System.currentTimeMillis();
    long elapsedSeconds = ((now-was)/1000) - 1;
    timeStamp = now;		// update
    
    if (turnNow==player1) {
      p1seconds -= elapsedSeconds;
      if (p1seconds<0)
	p1seconds=0;
      //Club.debug(player1+" - "+elapsedSeconds+" = "+p1seconds);
    } else if (turnNow==player2) {
      p2seconds -= elapsedSeconds;
      if (p2seconds<0)
	p2seconds=0;
      //Club.debug(player2+" - "+elapsedSeconds+" = "+p2seconds);
    } else
      Club.debug("Game.updateTimers whoseTurn? "+turnNow);
  }
  
  // -----------------------------------------------------------------

  /* Append names of players, in order, to buf, space separated.
   * If a position is unoccupied, append *. E.g.,
   * "paul wxy " or "* wxy " or "paul * " or "* * ".
   */
  public void showPlayers(StringBuffer buf) {
    buf.append( (Member.alive(player1) ? player1.toString() : "*")
		+" "+
		(Member.alive(player2) ? player2.toString() : "*") );
  }
  private void switchTurns() {
    turnNow = (turnNow==player1) ? player2 : player1;
  }
  public boolean bothHere() {
    return (Member.alive(player1) && Member.alive(player2));
  }
  public Game(Table table,Member player1) { // called by table.open
    this.table = table;
    initState(player1,null);
    this.practice = false;
    setTimer(30,3);		// default values
  }
  public boolean canStart(Member member) {
    if (member==null) return false;
    int role = member.getRole();
    //System.out.println("Game.canStart "+member+" "+role+" "+practice);
    return (role==0 && practice) || role==1 ||
      (gameTimer==30 && moveTimer==3 && role<2);
  }
  public boolean start() {	// called by StartCommand
    //System.out.println("Game.start "+player1+" "+player2);
    initState(player1,player2);
    player1name = (player1!=null) ? player1.lowercase() : "*";
    player2name = (player2!=null) ? player2.lowercase() : "*"; 
    player1info = Member.info(player1);
    player2info = Member.info(player2);
    timeStamp = System.currentTimeMillis();

    history.addElement("setup @"+table+" "+player1info+" "+player2info);
    history.addElement("timer @"+table+" "+gameTimer+" "+moveTimer);

    gameState = 'r';
    return true;
  }
  public void initState(Member player1, Member player2) {
    this.name      = "xiangqi";
    this.player1   = player1;
    this.player1name = (player1==null) ? "*" : player1.toString();
    this.player1info = player1name;
    this.player2   = player2;
    this.player2name = (player2==null) ? "*" : player2.toString();
    this.player2info = player2name;
    this.turnNow   = player1;
    this.validate  = true;
    this.debug     = false;
    this.gameState = 'b';
    this.draw	   = null;
    this.history.removeAllElements();
    for (int r=0; r<10; r++ )
      for (int c=0; c<9; c++ )
        this.board[r][c] = startBoard[9-r][c];
    posK = ('e'<<8)+1;
    posk = ('e'<<8)+10;
	
    p1secPrev = p2secPrev = 0; // previous seconds left
    
    savers.removeAllElements();
  }
  public Member getPlayer1() { return player1; }
  public Member getPlayer2() { return player2; }
  private Member opponent(Member member) {
    if (member==player1)
      return player2;
    else
      return player1;
  }
  public boolean hasPlayers() {
    //System.out.println("Game.hasPlayers "+player1+" "+player2+" "+practice);
    return player1!=null && (practice || player2!=null);
  }
  public int numPlayers() {
    int num = 0;
    if (player1 != null) num++;
    if (player2 != null) num++;
    return num;
  }
  private void endGame() {
    gameState = 'e';
    doSaves();
  }

  public void leave(Member player) { // called by table.leave
    if (player == player1 ) {
      player1 = null;
      endGame();
    } else if (player == player2) {
      player2 = null;
      endGame();
    }
  }
  /** Return true if should count on record, else false.
   */
  public boolean resign(Member player) { // called by table.resign
    if (player!=null && (player==player1 || player==player2)) {
      Member winner = opponent(player);
      history.addElement("resign @"+table+" "+player+" to "+winner);
      boolean wasOver = (gameState=='e');
      endGame();
      return((history.size()>5) && !wasOver);	// hack
    } else {
      log("resign requested but "+player+" is not here.");
      return false;
    }
  }
  public boolean practice(Member player,boolean state) {
    if (gameState=='b' && player==player1) {
      practice = state;
      //System.out.println("Game.practice "+player+" "+player1+" "+gameState);
      return true;
    } else {
      //System.out.println("Game.practice! "+player+" "+player1+" "+gameState);
      return false;
    }
  }
  public boolean draw(Member player) { // called by table.draw
    if (player!=player1 && player!=player2) {
      log("draw requested by non-player "+player);
      return false;		// game not over
    }
    if (draw==player) {
      //log("draw requested again by "+player);
      return false;		// game not over
    }
    Member other = (player==player1) ? player2 : player1;
    if (draw==null) {
      draw = player;		// record the request
      return false;		// game not over
    } else {
      Club.assert(draw==other);
      history.addElement("draw @"+table+" "+player1+" "+player2);
      endGame();
      return true;		// game over
    }
  }
  public void join(Member player) { // called by table.join
    if (player1==null) {
      player1 = player;
    } else if (player2==null) {
      player2 = player;
    } else {
      log("join requested but game is already full.");
      return;
    }
  }
  // Called by ResetCommand
  public boolean reset(Member player1, Member player2) {
    if (gameState=='r')
      return false;
    //System.out.println("Game.reset. "+player1+" "+player2);
    initState(player1,player2);
    //System.out.println("Game.reset= "+player1+" "+player2);
    setTimer(30,3);		// default values
    return true;
  }
  
  // -------------------------------------------------------------------
  final static char[][] startBoard = {
    // Columns a-i player1 left to right; Rows 1-10 player1 home out
    // Uppercase pieces are player1.
    // - King, Advisor, Elephant, chaRiot, Horse, Cannon, Pawn.
    // a    b    c    d    e    f    g    h    i
    { 'r', 'h', 'e', 'a', 'k', 'a', 'e', 'h', 'r' }, // 10
    { ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ' }, //  9
    { ' ', 'c', ' ', ' ', ' ', ' ', ' ', 'c', ' ' }, //  8
    { 'p', ' ', 'p', ' ', 'p', ' ', 'p', ' ', 'p' }, //  7
    { ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ' }, //  6
    { ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ' }, //  5
    { 'P', ' ', 'P', ' ', 'P', ' ', 'P', ' ', 'P' }, //  4
    { ' ', 'C', ' ', ' ', ' ', ' ', ' ', 'C', ' ' }, //  3
    { ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ' }, //  2
    { 'R', 'H', 'E', 'A', 'K', 'A', 'E', 'H', 'R' }  //  1
    // a    b    c    d    e    f    g    h    i
  };
  
  public void display(Client c) {
    StringBuffer buf = new StringBuffer(128);
    buf.append("# "+player1info+" "+player2info+"\n");
    for (int r=10; r>=1; r-- ) { 
      // player2 home row=10 at top, player1 row=1 at bottom
      buf.append("# "+this.board[r-1]+" "+r+"\n");
    }
    buf.append("# abcdefghi");
    c.send(buf);
  }

  /** Return info about game status for results logfile entry,
   * e.g., "T30.3-4-2 M6" says Timer was 30 minutes game timer per player,
   * 3 minute move timer, players 1 and 2 ended with 4 and 2 minutes
   * left respectively, and there were 6 moves made.
   */
  public String stats() {
    return "T"+gameTimer+"."+moveTimer+"-"+
      (p1seconds/60)+"-"+(p2seconds/60)+
      " M"+(history.size()-3);	// hack
  }

  // Called by table.observe:
  public void sendHistory(Member member) {
    Client c = member.getClient();
    StringBuffer buf = new StringBuffer(40*(history.size()+2));
    if (history.size()==0) {
      buf.append("setup @"+table+" "+Member.info(player1)+" "+
		 Member.info(player2)+"\n");
      buf.append("timer @"+table+" "+gameTimer+" "+moveTimer+"\n");
    } else {
      boolean done = false;
      String line="";
      for (int i=0; !done; i++) {
	if (i<history.size()) {
	  line = (String)history.elementAt(i);
	  buf.append(line+"\n");
	}
	else
	  done = true;
      }
    }
    //    if (line.startsWith("move") || line.startsWith("take"))
    buf.append("ready "+table.tag()); // sent them all
    c.send(buf);
  }

  /** Save game to disk on the server.
   * For use with GamePage member home page.
   * http://members.xiangqi.com/paul
   * ~xiangqi/data/games/p/paul/us/karl,wxy,1
   * http://members.xiangqi.com/pat/game.cgi/cn/sandy/chris/4/13b
   * ~xiangqi/data/games/p/pat/cn/sandy/chris/4/13b
   */
  private Vector savers = new Vector(4,4); // list of members saving this game

  /** Called by SaveCommand. */
  public void save(Member member) {
    if (member!=null) {
      String saver = member.lowercase();
      if (savers.contains(saver))
	log("Game.save already has "+saver);
      else {
	savers.addElement(saver);
	//log("Game.save "+saver);
      }
      if (gameState=='e')
	doSaves();
    }
  }

  /** Called by endGame and subsequent save commands. */
  private void doSaves() {
    if (history.size()<7) {
      return;
    }
    Enumeration es = savers.elements(); // saving Members
    while (es.hasMoreElements()) {
      String saver = (String)es.nextElement();
      //log("Game.save now "+saver);
      File dir, file;

      dir = new File("../data/games");
      if (!dir.exists() || !dir.isDirectory() || !dir.canWrite()) {
	log("No writable games directory "+dir.getAbsolutePath());
	return;
      }
      String ch = saver.substring(0,1);
      dir = new File(dir,ch);
      if (!dir.exists() && !dir.mkdir()) {
	log("Could not create letter directory "+dir);
	return;
      }
      dir = new File(dir,saver);
      if (!dir.exists() && !dir.mkdir()) {
	log("Could not create game saver directory "+dir);
	return;
      }
      if (!dir.isDirectory() || !dir.canWrite()) {
	log("No writable game saver directory "+dir);
	return;
      }

      // Decide if saver can have only file or many:
      boolean manyFiles = Member.premium(saver);

      // Use # temp name while writing, to avoid gamepage sweeper:
      file = fileNew(dir,"#"+player1name+","+player2name,manyFiles);
      try {
	FileWriter out = new FileWriter(file);
	Enumeration e = history.elements();
	while (e.hasMoreElements()) {
	  String action = (String)e.nextElement();
	  action = action.trim();
	  out.write(action+"\n",0,action.length()+1);
	}
	out.flush();
	out.close();
	File f = fileNew(dir,player1name+","+player2name,manyFiles);
	file.renameTo(f);	// ready for gamepage sweep now
	//log("saved "+f.getPath());
      } catch (Exception e1) {
	Club.warning(e1, "Error saving "+file);
      }
    }
    savers.removeAllElements();
  }

  /** Return a new File object in DIR of NAME but which does
   * not yet exist. Works by appending , and a number to NAME
   * until it finds a name which does not yet exist.
   * If MANYFILES is false, don't bother finding unique name.
   */
  private File fileNew(File dir, String name, boolean manyFiles) {
    if (!manyFiles)
      return new File(dir,name+",0");
    
    File file;
    int n = 0;
    do {
      file = new File(dir,name+","+n);
      n++;
    } while (file.exists() && (n<1000));
    return file;
  }
  private boolean fileExists(String path) {
    File f = new File(path);
    return f.exists();
  }
  
  // -------------------------------------------------------------------
  /// save, restore, listSaved
                                   
  /** Get the piece at COLUMN and ROW.
   * E.g., get('a',1) returns 'R' at game start.
   */
  private char get(int column, int row) throws Exception {
    assertSyntax( (column>='a' && column<='i' && row>=1 && row<=10),
		  "Game.get position outside range.");
    return board[row-1][column-'a'];
  }
  private char getSafe(int column, int row) {
    char piece;
    try { piece=get(column,row); }
    catch(Exception e) { 
      Club.warning(e,"Game.getSafe error");
      piece=' ';
    }
    return piece;
  }

  /** Set the piece at COLUMN and ROW to PIECE. E.g., set('a',1,'R'). */
  private void set(int column, int row, char piece) throws Exception {
    assertSyntax( (column>='a' && column<='i' && row>=1 && row<=10),
		  "Game.set position outside range.");
    assertSyntax( VALID_PIECES.indexOf(piece)>=0,
		  "Game.set piece ("+piece+") invalid.");
    board[row-1][column-'a'] = piece;
    if (piece=='K') posK = (column<<8)+row;
    else if (piece=='k') posk = (column<<8)+row;
  }
  private void setSafe(int column, int row, char piece) {
    try { set(column,row,piece); }
    catch(Exception e) { Club.warning(e,"Game.setSafe error"); }
  }
  
  // -------------------------------------------------------------------
  private char getPiece(String str) throws Exception {
    assertSyntax(str.length()==1, "PIECE should be a single character.");
    char piece = str.charAt(0);
    assertSyntax(VALID_PIECES.indexOf(piece)>=0, 
                 "PIECE must be one of "+VALID_PIECES+".");
    return piece;
  }
  private int getPosition(String str, char piece) throws Exception {
    int column='\0';   // should be a-i
    int row=0;         // should be 1-10

    if (str.length()>=2) {    
      column = (int)str.charAt(0);
      try { row = Integer.parseInt(str.substring(1)); }
      catch (NumberFormatException e) { row=0; }
    }
    
    assertSyntax( (column>='a' && column<='i' && row>=1 && row<=10),
                  "POSITION should be column a-i and row 1-10, e.g., e4.");
    assertRule( (piece==' ') || (piece == get(column,row)),
		"PIECE (" +piece +") not at POSITION (" + str +").");
    return (column<<8) + row;
  }
  
  // -------------------------------------------------------------------
  /** Client is requesting a certain move be made.
   * @param args example "1 P g6 g7 p 595 nice move"
   * @param buf for any return message
   * @return Club.SYN or Club.ERR or Club.OK or Club.CHK or 
   * or Club.WIN (move won game) or Club.STM (move stalemated the game).
   */
  public int move(String command, StringBuffer buf) {
    try {
      assertRule(gameState=='r', "Game has not yet been started.");
      StringTokenizer args = new StringTokenizer(command);
      int argc = args.countTokens();
      assertSyntax(argc>=6,
        "Usage: move TABLE NUMBER PIECE1 POS1 POS2 PIECE2 SECONDS [NOTES]");

      String token = args.nextToken();
      int    moveNum = Club.atoi(token);
      int    moveNumE = history.size()-1; // hack
      if (moveNum!=moveNumE) {
	buf.append("Expected move number "+moveNumE+", got "+token);
	return(Club.ERR);
      }
	  
      char   piece1    = getPiece(args.nextToken());    
      int    position1 = getPosition(args.nextToken(),piece1);
      String pos2str   = args.nextToken();
      String piece2str = args.nextToken();
      char   piece2    = (piece2str.equals("-") ? ' ' : getPiece(piece2str));
      int    position2 = getPosition(pos2str,piece2);

      int    seconds   = Club.atoi(args.nextToken());
      boolean secIncr  = false; // try to set more than I used last time?
      Member member, other;
      if (moveNum%2==1) {	// red player
	member = player1;
	other = player2;
	secIncr = seconds>p1secPrev;
	p1secPrev = seconds;
      } else {			// blue player
	member = player2;
	other = player1;
	secIncr = seconds>p2secPrev;
	p2secPrev = seconds;
      }
      if (seconds>0 && (((seconds/60) > gameTimer) || secIncr)) {
	//Club.log("Game.move invalid timer: "+seconds+" "+member+" "+other);
      }

      int c1 = position1>>8, r1 = position1&0xFF;
      int c2 = position2>>8, r2 = position2&0xFF;
  
      if (practice || isLegal(piece1,c1,r1,piece2,c2,r2)) {    
        set(c1,r1,' ');
        set(c2,r2,piece1);
        if (!practice && isCheck()) {
          set(c1,r1,piece1);
          set(c2,r2,piece2);
          buf.append("Move would leave you in check.");
          return Club.ERR;
        }
	String name = table.tag();
        history.addElement("move "+name+" "+command);
	// table move p1 pos1 pos2 p2 t
        switchTurns();
        if (isMate()) {
          switchTurns();
          history.addElement("win @"+table+" "+turnNow+" "+opponent(turnNow));
	  endGame();
          //log("checkmate");///
	  timeStamp = System.currentTimeMillis();
          return Club.WIN;
        } else if (isCheck()) {
          //log("check");///
	  updateTimers();
          return Club.CHK;
        } 
      }
      
    } catch (Exception e) {
      buf.append(e.getMessage());
      return Club.SYN; /// should differentiate Club.SYN from Club.ERR
    }
    updateTimers();
    return Club.OK;
  }
  
  private boolean isLegal(char piece1, int c1, int r1, 
                          char piece2, int c2, int r2)
    throws Exception {

    assertRule(piece2==' ' ||
               (Character.isLowerCase(piece1) !=
		Character.isLowerCase(piece2)),
               "Can not capture your own piece.");
    assertRule(isMine(piece1), "Can not move opponent piece.");
    assertRule((c1!=c2 || r1!=r2), "POS1 same as POS2.");
    assertRule((piece1 == get(c1,r1)),
               "PIECE1 (" +piece1 +") not at POS1 (" + (char)c1+r1 +").");
    assertRule((piece2 == get(c2,r2)),
               "PIECE2 (" +piece2 +") not at POS2 (" + (char)c2+r2 +").");
      
    int rdelta = Math.abs(r1-r2);
    int cdelta = Math.abs(c1-c2);
    int diag   = (rdelta==cdelta) ? rdelta : (rdelta>0 && cdelta>0) ? -1: 0;
    
    switch(Character.toUpperCase(piece1)) {

    case 'R': // chaRiot, rook (ju)
      assertRule(diag==0, "Chariot can not move diagonally.");
      assertRule(between(c1,r1,c2,r2)==0,
		 "Chariot can not move over pieces.");
      break;
      
    case 'H': // Horse, knight (ma)
      assertRule((cdelta==2 && rdelta==1) || (cdelta==1 && rdelta==2),
		 "Horse must move in an L shape.");
      assertRule((cdelta==2 && get(mid(c1,c2),r1)==' ') ||
                 (rdelta==2 && get(c1,mid(r1,r2))==' '),
		 "Horse is blocked.");
      break;
    
    case 'E': // Elephant, bishop, minister (xiang)
      assertRule(onSide(piece1,r2),
		 "Elephant can not cross the river.");
      assertRule(diag==2, "Elephant must move exactly two rows and columns.");
      assertRule(get(mid(c1,c2),mid(r1,r2))==' ', "Elephant is blocked.");
      break;

    case 'A': // Advisor, guard, counsellor (shi)
      assertRule(inPalace(piece1,r2,c2), "Advisor can not leave the palace.");
      assertRule(diag==1, "Advisor must move one position diagonally.");
      break;
      
    case 'K': // King, general (shuai)
      if (Character.toLowerCase(piece2)=='k' && (c1==c2) && 
          between(c1,r1,c2,r2)==0)
        break; // King check king ok
      assertRule(inPalace(piece1,r2,c2), "King can not leave the palace.");
      assertRule(diag==0, "King can not move diagonally.");
      assertRule(cdelta<=1 && rdelta<=1, "King can only move one position.");
      break;
      
    case 'C': // Cannon
      assertRule(diag==0, "Cannon can not move diagonally.");
      int over = between(c1,r1,c2,r2);
      assertRule(over<=1, "Cannon can use only one catapult.");
      assertRule((over==1)!=(piece2==' '),
		 "Cannon must catapult to capture.");
      break;
      
    case 'P': // Pawn
      assertRule(diag==0, "Pawn can not move diagonally.");
      assertRule(!backward(piece1,r1,r2), "Pawn can not move backward.");
      if (onSide(piece1,r2))
	assertRule(cdelta==0 && rdelta==1,
		   "Pawn before river can only move one forward.");
      else
	assertRule(cdelta<=1 && rdelta<=1,
		   "Pawn can only move one position.");
      break;
        
    default:
      assertRule(false,
		 "PIECE1 ("+piece1+") must be one of "+VALID_PIECES+".");
    }
    return true;
  }
  private boolean isLegalSafe(char piece1, int c1, int r1, 
                              char piece2, int c2, int r2) {
    boolean legal;
    try { legal = isLegal(piece1,c1,r1, piece2,c2,r2); }
    catch(Exception e) { 
      //if(piece1=='C') e.printStackTrace();
      legal = false; 
    }
    return legal;
  }                              
  
    
  // -------------------------------------------------------------------
  /** Has the player whose turn it is now just been put in check? */
  private boolean isCheck() {
    char king = (turnNow==player1) ? 'K' : 'k';
    int  kpos = (turnNow==player1) ? posK : posk;
    int  kc   = kpos>>8, kr = kpos&0xFF;

    // See if any opponent (player just moved) piece could now kill my king:
    switchTurns();
    for (int column='a'; column<='i'; column++ )
      for (int row=1; row<=10; row++ ) {
        char piece = getSafe(column,row);
        if ( isMine(piece) ) {
          if ( isLegalSafe(piece,column,row, king,kc,kr) ) {
	    //System.out.println(piece+"@"+column+row+" "+king+"@"+kc+kr);
            switchTurns();
            return true;
          }
        }
      }
    debug("isCheck no "+turnNow+" move to kill king at "+(char)kc+kr);
    switchTurns();
    return false;
  }
  
  // -------------------------------------------------------------------
  /** Has the player whose turn it is now just been put into checkmate? */
  private boolean isMate() {
    // See if the player can make any move without being in check.
    for (int c1='a'; c1<='i'; c1++ )
      for (int r1=1; r1<=10; r1++ ) {
        char piece1 = getSafe(c1,r1);
        if (isMine(piece1)) {
          ///debug("Test move my "+piece1+" at "+(char)c1+r1);
          for (int c2='a'; c2<='i'; c2++ )
            for (int r2=1; r2<=10; r2++ ) {
              char piece2 = getSafe(c2,r2);
              if (isLegalSafe(piece1,c1,r1, piece2,c2,r2)) {
                setSafe(c1,r1,' ');
                setSafe(c2,r2,piece1);
                boolean check=isCheck();
                setSafe(c1,r1,piece1);
                setSafe(c2,r2,piece2);
                if (!check) {
                  debug("isMate false, could move "+piece1+" from "+
			(char)c1+r1+ " to "+(char)c2+r2);
                  return false;
                }
              }
            }
        }
      }
    endGame();
    draw = null;
    return true;
  }
  
  // -------------------------------------------------------------------
  //private class GameSyntaxException extends Exception {}
  //private class GameErrorException extends Exception {}
  
  private void assertRule(boolean truth, String errorMessage) 
    throws Exception {
    if (validate && !truth) throw new Exception(errorMessage);
  }
  private void assertSyntax(boolean truth, String errorMessage) 
    throws Exception {
    if (validate && !truth) throw new Exception(errorMessage);
  }
  
  // -------------------------------------------------------------------

  private boolean isMine(char piece) {
    return ( piece!=' ' && Character.isUpperCase(piece)==(turnNow==player1) );
  }
  private static boolean backward(char piece, int r1, int r2) {
    return ( (Character.isUpperCase(piece) && r1>r2) ||
	     (Character.isLowerCase(piece) && r2>r1) );
  }
  private static boolean onSide(char piece, int row) {
    return ( (Character.isUpperCase(piece) && row<6) ||
             (Character.isLowerCase(piece) && row>5) );
  }
  private static boolean inPalace(char piece, int row, int column) {
    if ( (column<'d') || (column>'f') )
      return false; // moved beside palace!
    if ( (Character.isUpperCase(piece) && (row>3)) ||
         (Character.isLowerCase(piece) && (row<8)) )
      return false; // moved ahead of palace!
    return true;
  }
  /** @return number of pieces between positions in line */
  private int between(int c1, int r1, int c2, int r2) throws Exception {
    int count = 0;
    if (c1==c2) { // vertical move
      for (int r = mid(r1,r2); r<max(r1,r2); r++ )
        if (get(c1,r)!=' ')
          count++;
    }
    else if (r1==r2) { // horizontal move
      for (int c = mid(c1,c2); c<max(c1,c2); c++ )
        if (get(c,r1)!=' ')
          count++;
    }
    else
      log("between called with a diagonal.");
    return count;
  }
  private static int delta(int i, int j) {
    int r = i-j; return r<0 ? -r : r;
  }
  private static int min(int i, int j)   { return (i<j) ? i : j; }
  private static int mid(int i, int j)   { return min(i,j)+1; }
  private static int max(int i, int j)   { return (i>j) ? i : j; }
  private boolean isEven(int i) { return (i%2)==0; }
  private static String hex(int i) {
    return "0x"+Integer.toHexString(i);
  }

  private void log(String message) {
    Club.log("game "+player1+" vs. "+player2+": "+message);
  }

  private void debug(String message) {
    if (debug)
      System.out.println(message);
  }
}
