/**
* Jin - a chess client for internet chess servers.
* More information is available at http://www.jinchess.com/.
* Copyright (C) 2002, 2003 Alexander Maryanovsky.
* All rights reserved.
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
*/
package free.jin.freechess;
import java.io.IOException;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.net.Socket;
import java.util.Enumeration;
import java.util.Hashtable;
import java.util.Vector;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.swing.SwingUtilities;
import free.chess.*;
import free.chess.variants.BothSidesCastlingVariant;
import free.chess.variants.NoCastlingVariant;
import free.chess.variants.atomic.Atomic;
import free.chess.variants.fischerrandom.FischerRandom;
import free.chess.variants.suicide.Suicide;
import free.chessclub.ChessclubConnection;
import free.freechess.*;
import free.jin.*;
import free.jin.event.*;
import free.jin.freechess.event.IvarStateChangeEvent;
import free.util.Pair;
import free.util.TextUtilities;
/**
* An implementation of the JinConnection interface for the freechess.org
* server.
*/
public class JinFreechessConnection extends FreechessConnection implements Connection,
SeekConnection, PGNConnection{
/**
* Our listener manager.
*/
private final FreechessListenerManager listenerManager = new FreechessListenerManager(this);
/**
* Creates a new JinFreechessConnection with the specified hostname, port,
* requested username and password.
*/
public JinFreechessConnection(String requestedUsername, String password){
super(requestedUsername, password, System.out);
setInterface(Jin.getInstance().getAppName() + " " + Jin.getInstance().getAppVersion() +
" (" + System.getProperty("java.vendor") + " " + System.getProperty("java.version") +
", " + System.getProperty("os.name") + " " + getSafeOSVersion() + ")");
setStyle(12);
setIvarState(Ivar.GAMEINFO, true);
setIvarState(Ivar.SHOWOWNSEEK, true);
setIvarState(Ivar.PENDINFO, true);
setIvarState(Ivar.MOVECASE, true);
// setIvarState(Ivar.COMPRESSMOVE, true); Pending DAV's bugfixing spree
setIvarState(Ivar.LOCK, true);
}
/**
* Returns the OS version after stripping out the patch level from it.
* We do this to avoid revealing that information to everyone on the server.
*/
private static String getSafeOSVersion(){
String osVersion = System.getProperty("os.version");
int i = osVersion.indexOf(".", osVersion.indexOf(".") + 1);
if (i != -1)
osVersion = osVersion.substring(0, i) + ".x";
return osVersion;
}
/**
* Returns a Player object corresponding to the specified string. If the
* string is "W", returns <code>Player.WHITE</code>. If it's "B", returns
* <code>Player.BLACK</code>. Otherwise, throws an IllegalArgumentException.
*/
public static Player playerForString(String s){
if (s.equals("B"))
return Player.BLACK_PLAYER;
else if (s.equals("W"))
return Player.WHITE_PLAYER;
else
throw new IllegalArgumentException("Bad player string: "+s);
}
/**
* Returns our ListenerManager.
*/
public ListenerManager getListenerManager(){
return getFreechessListenerManager();
}
/**
* Returns out ListenerManager as a reference to FreechessListenerManager.
*/
public FreechessListenerManager getFreechessListenerManager(){
return listenerManager;
}
/**
* Fires an "attempting" connection event and invokes {@link free.util.Connection#initiateConnect(String, int)}.
*/
public void initiateConnectAndLogin(String hostname, int port){
listenerManager.fireConnectionAttempted(this, hostname, port);
initiateConnect(hostname, port);
}
/**
* Fires an "established" connection event.
*/
protected void handleConnected(){
listenerManager.fireConnectionEstablished(this);
super.handleConnected();
}
/**
* Fires a "failed" connection event.
*/
protected void handleConnectingFailed(IOException e){
listenerManager.fireConnectingFailed(this, e.getMessage());
super.handleConnectingFailed(e);
}
/**
* Fires a "login succeeded" connection event and performs other on-login tasks.
*/
protected void handleLoginSucceeded(){
super.handleLoginSucceeded();
sendCommand("$set bell 0");
filterLine("Bell off.");
listenerManager.fireLoginSucceeded(this);
}
/**
* Fires a "login failed" connection event.
*/
protected void handleLoginFailed(String reason){
listenerManager.fireLoginFailed(this, reason);
super.handleLoginFailed(reason);
}
/**
* Fires a "connection lost" connection event.
*/
protected void handleDisconnection(IOException e){
listenerManager.fireConnectionLost(this);
super.handleDisconnection(e);
}
/**
* Overrides {@link free.util.Connection#connectImpl(String, int)} to return a timesealing socket.
*/
protected Socket connectImpl(String hostname, int port) throws IOException{
Socket result = null;
try{
Class tsSocketClass = Class.forName("free.freechess.timeseal.TimesealingSocket");
Constructor tsSocketConstructor = tsSocketClass.getConstructor(new Class[]{String.class, int.class});
result = (Socket)tsSocketConstructor.newInstance(new Object[]{hostname, new Integer(port)});
} catch (ClassNotFoundException e){}
catch (SecurityException e){}
catch (NoSuchMethodException e){}
catch (IllegalArgumentException e){}
catch (InstantiationException e){}
catch (IllegalAccessException e){}
catch (InvocationTargetException e){
Throwable targetException = e.getTargetException();
if (targetException instanceof IOException)
throw (IOException)targetException;
else if (targetException instanceof RuntimeException)
throw (RuntimeException)targetException;
else if (targetException instanceof Error)
throw (Error)targetException;
else
e.printStackTrace(); // Shouldn't happen, I think
}
if (result == null)
result = new Socket(hostname, port);
return result; }
/**
* Notifies any interested PlainTextListener of the received line of otherwise
* unidentified text.
*/
protected void processLine(String line){
listenerManager.firePlainTextEvent(new PlainTextEvent(this, line));
}
/**
* Gets called when the server notifies us of a change in the state of some
* ivar.
*/
protected boolean processIvarStateChanged(Ivar ivar, boolean state){
IvarStateChangeEvent evt = new IvarStateChangeEvent(this, ivar, state);
listenerManager.fireIvarStateChangeEvent(evt);
return false;
}
/**
* Fires an appropriate ChatEvent.
*/
protected boolean processPersonalTell(String username, String titles, String message){
listenerManager.fireChatEvent(new ChatEvent(this, "tell", ChatEvent.PERSON_TO_PERSON_CHAT_CATEGORY,
username, (titles == null ? "" : titles), -1, message, null));
return true;
}
/**
* Fires an appropriate ChatEvent.
*/
protected boolean processSayTell(String username, String titles, int gameNumber, String message){
listenerManager.fireChatEvent(new ChatEvent(this, "say", ChatEvent.PERSON_TO_PERSON_CHAT_CATEGORY,
username, (titles == null ? "" : titles), -1, message, new Integer(gameNumber)));
return true;
}
/**
* Fires an appropriate ChatEvent.
*/
protected boolean processPTell(String username, String titles, String message){
listenerManager.fireChatEvent(new ChatEvent(this, "ptell", ChatEvent.PERSON_TO_PERSON_CHAT_CATEGORY,
username, (titles == null ? "" : titles), -1, message, null));
return true;
}
/**
* Fires an appropriate ChatEvent.
*/
protected boolean processChannelTell(String username, String titles, int channelNumber,
String message){
listenerManager.fireChatEvent(new ChatEvent(this, "channel-tell", ChatEvent.ROOM_CHAT_CATEGORY,
username, (titles == null ? "" : titles), -1, message, new Integer(channelNumber)));
return true;
}
/**
* Fires an appropriate ChatEvent.
*/
protected boolean processKibitz(String username, String titles, int rating, int gameNumber,
String message){
if (titles == null)
titles = "";
listenerManager.fireChatEvent(new ChatEvent(this, "kibitz", ChatEvent.GAME_CHAT_CATEGORY,
username, titles, rating, message, new Integer(gameNumber)));
return true;
}
/**
* Fires an appropriate ChatEvent.
*/
protected boolean processWhisper(String username, String titles, int rating, int gameNumber,
String message){
if (titles == null)
titles = "";
listenerManager.fireChatEvent(new ChatEvent(this, "whisper", ChatEvent.GAME_CHAT_CATEGORY,
username, titles, rating, message, new Integer(gameNumber)));
return true;
}
/**
* Regex for matching tourney tell qtells.
*/
private static final Pattern TOURNEY_TELL_REGEX =
Pattern.compile("^("+USERNAME_REGEX+")("+TITLES_REGEX+")?\\(T(\\d+)\\): (.*)");
/**
* Fires an appropriate ChatEvent.
*/
protected boolean processQTell(String message){
ChatEvent evt;
Matcher matcher = TOURNEY_TELL_REGEX.matcher(message);
if (matcher.matches()){
String sender = matcher.group(1);
String title = matcher.group(2);
if (title == null)
title = "";
Integer tourneyIndex = new Integer(matcher.group(3));
message = matcher.group(4);
evt = new ChatEvent(this, "qtell.tourney", ChatEvent.TOURNEY_CHAT_CATEGORY,
sender, title, -1, message, tourneyIndex);
}
else{
evt = new ChatEvent(this, "qtell", ChatEvent.PERSON_TO_PERSON_CHAT_CATEGORY,
null, null, -1, message, null);
}
listenerManager.fireChatEvent(evt);
return true;
}
/**
* Fires an appropriate ChatEvent.
*/
protected boolean processShout(String username, String titles, String message){
listenerManager.fireChatEvent(new ChatEvent(this, "shout", ChatEvent.ROOM_CHAT_CATEGORY,
username, (titles == null ? "" : titles), -1, message, null));
return true;
}
/**
* Fires an appropriate ChatEvent.
*/
protected boolean processIShout(String username, String titles, String message){
listenerManager.fireChatEvent(new ChatEvent(this, "ishout", ChatEvent.ROOM_CHAT_CATEGORY,
username, (titles == null ? "" : titles), -1, message, null));
return true;
}
/**
* Fires an appropriate ChatEvent.
*/
protected boolean processTShout(String username, String titles, String message){
listenerManager.fireChatEvent(new ChatEvent(this, "tshout", ChatEvent.TOURNEY_CHAT_CATEGORY,
username, (titles == null ? "" : titles), -1, message, null));
return true;
}
/**
* Fires an appropriate ChatEvent.
*/
protected boolean processCShout(String username, String titles, String message){
listenerManager.fireChatEvent(new ChatEvent(this, "cshout", ChatEvent.ROOM_CHAT_CATEGORY,
username, (titles == null ? "" : titles), -1, message, null));
return true;
}
/**
* Fires an appropriate ChatEvent.
*/
protected boolean processAnnouncement(String username, String message){
listenerManager.fireChatEvent(new ChatEvent(this, "announcement", ChatEvent.BROADCAST_CHAT_CATEGORY,
username, "", -1, message, null));
return true;
}
/**
* Returns the wild variant corresponding to the given server wild variant
* name/category name, or <code>null</code> if that category is not supported.
*/
private static WildVariant getVariant(String categoryName){
if (categoryName.equalsIgnoreCase("lightning") ||
categoryName.equalsIgnoreCase("blitz") ||
categoryName.equalsIgnoreCase("standard") ||
categoryName.equalsIgnoreCase("untimed"))
return Chess.getInstance();
if (categoryName.startsWith("wild/")){
String wildId = categoryName.substring("wild/".length());
if (wildId.equals("0") || wildId.equals("1"))
return new BothSidesCastlingVariant(Chess.INITIAL_POSITION_FEN, categoryName);
else if (wildId.equals("2") || wildId.equals("3"))
return new NoCastlingVariant(Chess.INITIAL_POSITION_FEN, categoryName);
else if (wildId.equals("5") || wildId.equals("8") || wildId.equals("8a"))
return new ChesslikeGenericVariant(Chess.INITIAL_POSITION_FEN, categoryName);
else if (wildId.equals("fr"))
return FischerRandom.getInstance();
}
else if (categoryName.equals("suicide"))
return Suicide.getInstance();
else if (categoryName.equals("losers"))
return new ChesslikeGenericVariant(Chess.INITIAL_POSITION_FEN, categoryName);
else if (categoryName.equals("atomic"))
return Atomic.getInstance();
// This means it's a fake variant we're using because the server hasn't told us the real one.
else if (categoryName.equals("Unknown variant"))
return Chess.getInstance();
return null;
}
/**
* Returns the wild variant name corresponding to the specified wild variant,
* that can be used for issuing a seek, e.g. "w1" or "fr".
* Returns null if the specified wild variant is not supported by FICS.
*/
private String getWildName(WildVariant variant){
if (variant == null)
throw new IllegalArgumentException("Null variant");
String variantName = variant.getName();
if (variantName.startsWith("wild/"))
return "w" + variantName.substring("wild/".length());
else if (variant.equals(Chess.getInstance()))
return "";
else if (variant.equals(FischerRandom.getInstance()))
return "fr";
else if (variant.equals(Suicide.getInstance()))
return "suicide";
else if (variant.equals(Atomic.getInstance()))
return "atomic";
else if ("losers".equals(variantName))
return "losers";
return null;
}
/**
* A list of supported wild variants, initialized lazily.
*/
private static WildVariant [] wildVariants;
/**
* Returns a list of support wild variants.
*/
public WildVariant [] getSupportedVariants(){
if (wildVariants == null){
wildVariants = new WildVariant[]{
Chess.getInstance(),
FischerRandom.getInstance(),
Suicide.getInstance(),
Atomic.getInstance(),
new ChesslikeGenericVariant(Chess.INITIAL_POSITION_FEN, "losers"),
new BothSidesCastlingVariant(Chess.INITIAL_POSITION_FEN, "wild/0"),
new BothSidesCastlingVariant(Chess.INITIAL_POSITION_FEN, "wild/1"),
new NoCastlingVariant(Chess.INITIAL_POSITION_FEN, "wild/2"),
new NoCastlingVariant(Chess.INITIAL_POSITION_FEN, "wild/3"),
new ChesslikeGenericVariant(Chess.INITIAL_POSITION_FEN, "wild/5"),
new ChesslikeGenericVariant(Chess.INITIAL_POSITION_FEN, "wild/8"),
new ChesslikeGenericVariant(Chess.INITIAL_POSITION_FEN, "wild/8a"),
};
}
return (WildVariant [])wildVariants.clone();
}
/**
* A hashtable where we keep game numbers mapped to GameInfoStruct objects
* of games that haven't started yet.
*/
private final Hashtable unstartedGamesData = new Hashtable(1);
/**
* Maps game numbers to InternalGameData objects of ongoing games.
*/
private final Hashtable ongoingGamesData = new Hashtable(5);
/**
* A Hashtable mapping Game objects to Vectors of moves which were sent for
* these games but the server didn't tell us yet whether the move is legal
* or not.
*/
private final Hashtable unechoedMoves = new Hashtable(1);
/**
* A list of game numbers of ongoing games which we can't support for some
* reason (not a supported variant for example).
*/
private final Vector unsupportedGames = new Vector();
/**
* The user's primary played (by the user) game, -1 if unknown. This is only
* set when the user is playing more than one game.
*/
private int primaryPlayedGame = -1;
/**
* The user's primary observed game, -1 if unknown. This is only set when
* the user is observing more than one game.
*/
private int primaryObservedGame = -1;
/**
* Returns the game with the specified number.
* This method (currently) exists solely for the benefit of the arrow/circle
* script.
*/
public Game getGame(int gameNumber) throws NoSuchGameException{
return getGameData(gameNumber).game;
}
/**
* Returns the InternalGameData for the ongoing game with the specified
* number. Throws a <code>NoSuchGameException</code> if there's no such game.
*/
private InternalGameData getGameData(int gameNumber) throws NoSuchGameException{
InternalGameData gameData = (InternalGameData)ongoingGamesData.get(new Integer(gameNumber));
if (gameData == null)
throw new NoSuchGameException();
return gameData;
}
/**
* Finds the (primary) game played by the user. Throws a
* <code>NoSuchGameException</code> if there's no such game.
*/
private InternalGameData findMyGame() throws NoSuchGameException{
if (primaryPlayedGame != -1)
return getGameData(primaryPlayedGame);
Enumeration gameNumbers = ongoingGamesData.keys();
while (gameNumbers.hasMoreElements()){
Integer gameNumber = (Integer)gameNumbers.nextElement();
InternalGameData gameData = (InternalGameData)ongoingGamesData.get(gameNumber);
Game game = gameData.game;
if (game.getGameType() == Game.MY_GAME)
return gameData;
}
throw new NoSuchGameException();
}
/**
* Finds the played user's game against the specified opponent.
* Returns the game number of null if no such game exists.
*/
private InternalGameData findMyGameAgainst(String playerName) throws NoSuchGameException{
Enumeration gameNumbers = ongoingGamesData.keys();
while (gameNumbers.hasMoreElements()){
Integer gameNumber = (Integer)gameNumbers.nextElement();
InternalGameData gameData = (InternalGameData)ongoingGamesData.get(gameNumber);
Game game = gameData.game;
Player userPlayer = game.getUserPlayer();
if (userPlayer == null) // Not our game or not played
continue;
Player oppPlayer = userPlayer.getOpponent();
if ((oppPlayer.isWhite() && game.getWhiteName().equals(playerName)) ||
(oppPlayer.isBlack() && game.getBlackName().equals(playerName)))
return gameData;
}
throw new NoSuchGameException();
}
/**
* Saves the GameInfoStruct until we receive enough info to fire a
* GameStartEvent.
*/
protected boolean processGameInfo(GameInfoStruct data){
unstartedGamesData.put(new Integer(data.getGameNumber()), data);
return true;
}
/**
* Fires an appropriate GameEvent depending on the situation.
*/
protected boolean processStyle12(Style12Struct boardData){
Integer gameNumber = new Integer(boardData.getGameNumber());
InternalGameData gameData = (InternalGameData)ongoingGamesData.get(gameNumber);
GameInfoStruct unstartedGameInfo = (GameInfoStruct)unstartedGamesData.remove(gameNumber);
if (unstartedGameInfo != null) // A new game
gameData = startGame(unstartedGameInfo, boardData);
else if (gameData != null){ // A known game
Style12Struct oldBoardData = gameData.boardData;
int plyDifference = boardData.getPlayedPlyCount() - oldBoardData.getPlayedPlyCount();
if (plyDifference < 0)
tryIssueTakeback(gameData, boardData);
else if (plyDifference == 0){
if (!oldBoardData.getBoardFEN().equals(boardData.getBoardFEN()))
changePosition(gameData, boardData);
// This happens if you:
// 1. Issue "refresh".
// 2. Make an illegal move, because the server will re-send us the board
// (although we don't need it)
// 3. Issue board setup commands.
// 4. Use "wname" or "bname" to change the names of the white or black
// players.
}
else if (plyDifference == 1){
if (boardData.getMoveVerbose() != null)
makeMove(gameData, boardData);
else
changePosition(gameData, boardData);
// This shouldn't happen, but I'll leave it just in case
}
else if (plyDifference > 1){
changePosition(gameData, boardData);
// This happens if you:
// 1. Issue "forward" with an argument of 2 or bigger.
}
}
else if (!unsupportedGames.contains(gameNumber)){
// Grr, the server started a game without sending us a GameInfo line.
// Currently happens if you start examining a game (26.08.2002), or
// doing "refresh <game>" (04.07.2004).
// We have no choice but to fake the data, since the server simply doesn't
// send us this information.
GameInfoStruct fakeGameInfo = new GameInfoStruct(boardData.getGameNumber(),
false, "Unknown variant", false, false, false, boardData.getInitialTime(),
boardData.getIncrement(), boardData.getInitialTime(), boardData.getIncrement(),
0, -1, ' ', -1, ' ', false, false);
gameData = startGame(fakeGameInfo, boardData);
}
if (gameData != null)
updateGame(gameData, boardData);
return true;
}
/**
* Processes a delta-board. Instead of actually handing the delta-board, this
* method, instead, creates a Style12Struct object and then asks
* <code>processStyle12</code> to handle it.
*/
protected boolean processDeltaBoard(DeltaBoardStruct data){
Integer gameNumber = new Integer(data.getGameNumber());
InternalGameData gameData = (InternalGameData)ongoingGamesData.get(gameNumber);
Game game = gameData.game;
if (game.getVariant() != Chess.getInstance())
throw new IllegalStateException("delta-boards should only be sent for regular chess");
Style12Struct lastBoardData = gameData.boardData;
Vector moveList = gameData.moveList;
Position pos = game.getInitialPosition();
for (int i = 0; i < moveList.size(); i++)
pos.makeMove((Move)moveList.elementAt(i));
ChessMove move = (ChessMove)(Move.parseWarrenSmith(data.getMoveSmith(), pos, data.getMoveAlgebraic()));
Square startSquare = move.getStartingSquare();
ChessPiece movingPiece = (ChessPiece)((startSquare == null) ? null : pos.getPieceAt(startSquare));
pos.makeMove(move);
String boardLexigraphic = pos.getLexigraphic();
String currentPlayer = pos.getCurrentPlayer().isWhite() ? "W" : "B";
int doublePawnPushFile = move.getDoublePawnPushFile();
boolean kingMoved = movingPiece.isKing();
boolean canWhiteCastleKingside =
lastBoardData.canWhiteCastleKingside() && !kingMoved && !Square.getInstance(7, 0).equals(startSquare);
boolean canWhiteCastleQueenside =
lastBoardData.canBlackCastleQueenside() && !kingMoved && !Square.getInstance(0, 0).equals(startSquare);
boolean canBlackCastleKingside =
lastBoardData.canBlackCastleKingside() && !kingMoved && !Square.getInstance(7, 7).equals(startSquare);
boolean canBlackCastleQueenside =
lastBoardData.canBlackCastleQueenside() && !kingMoved && !Square.getInstance(0, 7).equals(startSquare);
boolean isIrreversibleMove = movingPiece.isPawn() || move.isCapture() ||
(canWhiteCastleKingside != lastBoardData.canWhiteCastleKingside()) ||
(canWhiteCastleQueenside != lastBoardData.canWhiteCastleQueenside()) ||
(canBlackCastleKingside != lastBoardData.canBlackCastleKingside()) ||
(canBlackCastleQueenside != lastBoardData.canBlackCastleQueenside());
int pliesSinceIrreversible = isIrreversibleMove ? 0 : lastBoardData.getPliesSinceIrreversible() + 1;
String whiteName = lastBoardData.getWhiteName();
String blackName = lastBoardData.getBlackName();
int gameType = lastBoardData.getGameType();
boolean isPlayedGame = lastBoardData.isPlayedGame();
boolean isMyTurn = pos.getCurrentPlayer() == game.getUserPlayer();
int initTime = lastBoardData.getInitialTime();
int inc = lastBoardData.getIncrement();
int whiteStrength = calcStrength(pos, Player.WHITE_PLAYER);
int blackStrength = calcStrength(pos, Player.BLACK_PLAYER);
int whiteTime = pos.getCurrentPlayer().isBlack() ? data.getRemainingTime() : lastBoardData.getWhiteTime();
int blackTime = pos.getCurrentPlayer().isWhite() ? data.getRemainingTime() : lastBoardData.getBlackTime();
int nextMoveNumber = lastBoardData.getNextMoveNumber() + (pos.getCurrentPlayer().isWhite() ? 1 : 0);
String moveVerbose = createVerboseMove(pos, move);
String moveSAN = data.getMoveAlgebraic();
int moveTime = data.getTakenTime();
boolean isBoardFlipped = lastBoardData.isBoardFlipped();
boolean isClockRunning = true;
int lag = 0; // The server doesn't currently send us this information
Style12Struct boardData = new Style12Struct(boardLexigraphic, currentPlayer, doublePawnPushFile,
canWhiteCastleKingside, canWhiteCastleQueenside, canBlackCastleKingside, canBlackCastleQueenside,
pliesSinceIrreversible, gameNumber.intValue(), whiteName, blackName, gameType, isPlayedGame,
isMyTurn, initTime, inc, whiteStrength, blackStrength, whiteTime, blackTime, nextMoveNumber,
moveVerbose, moveSAN, moveTime, isBoardFlipped, isClockRunning, lag);
processStyle12(boardData);
return true;
}
/**
* Calculates the material strength of the specified player in the specified
* position.
*/
private static int calcStrength(Position pos, Player player){
int count = 0;
for (int i = 0; i < 8; i++){
for (int j = 0; j < 8; j++){
ChessPiece piece = (ChessPiece)(pos.getPieceAt(i, j));
if ((piece != null) && (piece.getPlayer() == player)){
if (piece.isPawn())
count += 1;
else if (piece.isBishop())
count += 3;
else if (piece.isKnight())
count += 3;
else if (piece.isRook())
count += 5;
else if (piece.isQueen())
count += 9;
else if (piece.isKing())
count += 0;
}
}
}
return count;
}
/**
* Creates a verbose representation of the specified move in the specified
* position. The move has already been made in the position.
*/
private static String createVerboseMove(Position pos, ChessMove move){
if (move.isShortCastling())
return "o-o";
else if (move.isLongCastling())
return "o-o-o";
else{
ChessPiece piece = (ChessPiece)pos.getPieceAt(move.getEndingSquare());
String moveVerbose = piece.toShortString() + "/" + move.getStartingSquare() + "-" + move.getEndingSquare();
if (move.isPromotion())
return moveVerbose + "=" + move.getPromotionTarget().toShortString();
else
return moveVerbose;
}
}
/**
* Changes the bsetup state of the game.
*/
protected boolean processBSetupMode(boolean entered){
try{
findMyGame().isBSetup = entered;
} catch (NoSuchGameException e){}
return super.processBSetupMode(entered);
}
/**
* A small class for keeping internal data about a game.
*/
private static class InternalGameData{
/**
* The Game object representing the game.
*/
public final Game game;
/**
* A list of Moves done in the game.
*/
public Vector moveList = new Vector();
/**
* The last Style12Struct we got for this game.
*/
public Style12Struct boardData = null;
/**
* Is this game in bsetup mode?
*/
public boolean isBSetup = false;
/**
* Maps offer indices to offers. Offers are Pairs where the first element
* is the <code>Player</code> who made the offer and the 2nd is the offer
* id. Takeback offers are kept separately.
*/
public final Hashtable indicesToOffers = new Hashtable();
/**
* Maps takeback offer indices to takeback offers. Takeback offers are Pairs
* where the first element is the <code>Player</code> who made the offer
* and the 2nd is an <code>Integer</code> specifying the amount of plies
* offered to take back.
*/
public final Hashtable indicesToTakebackOffers = new Hashtable();
/**
* Works as a set of the offers currently in this game. The elements are
* Pairs in which the first item is the <code>Player</code> who made the
* offer and the second one is the offer id. Takeback offers are kept
* separately.
*/
private final Hashtable offers = new Hashtable();
/**
* The number of plies the white player offerred to takeback.
*/
private int whiteTakeback;
/**
* The number of plies the black player offerred to takeback.
*/
private int blackTakeback;
/**
* Creates a new InternalGameData.
*/
public InternalGameData(Game game){
this.game = game;
}
/**
* Returns the amount of moves made in the game (as far as we counted).
*/
public int getMoveCount(){
return moveList.size();
}
/**
* Adds the specified move to the moves list.
*/
public void addMove(Move move){
moveList.addElement(move);
}
/**
* Removes the last <code>count</code> moves from the movelist, if possible.
* Otherwise, throws an <code>IllegalArgumentException</code>.
*/
public void removeLastMoves(int count){
if (count > moveList.size())
throw new IllegalArgumentException("Can't remove more elements than there are elements");
int first = moveList.size() - 1;
int last = moveList.size() - count;
for (int i = first; i >= last; i--)
moveList.removeElementAt(i);
}
/**
* Removes all the moves made in the game.
*/
public void clearMoves(){
moveList.removeAllElements();
}
/**
* Returns true if the specified offer is currently made by the specified
* player in this game.
*/
public boolean isOffered(int offerId, Player player){
return offers.containsKey(new Pair(player, new Integer(offerId)));
}
/**
* Sets the state of the specified offer in the game. Takeback offers are
* handled by the setTakebackCount method.
*/
public void setOffer(int offerId, Player player, boolean isMade){
Pair offer = new Pair(player, new Integer(offerId));
if (isMade)
offers.put(offer, offer);
else
offers.remove(offer);
}
/**
* Sets the takeback offer in the game to the specified amount of plies.
*/
public void setTakebackOffer(Player player, int plies){
if (player.isWhite())
whiteTakeback = plies;
else
blackTakeback = plies;
}
/**
* Returns the amount of plies offered to take back by the specified player.
*/
public int getTakebackOffer(Player player){
if (player.isWhite())
return whiteTakeback;
else
return blackTakeback;
}
}
/**
* Changes the primary played game.
*/
protected boolean processSimulCurrentBoardChanged(int gameNumber, String oppName){
primaryPlayedGame = gameNumber;
return true;
}
/**
* Changes the primary observed game.
*/
protected boolean processPrimaryGameChanged(int gameNumber){
primaryObservedGame = gameNumber;
return true;
}
/**
* Invokes <code>closeGame(int)</code>.
*/
protected boolean processGameEnd(int gameNumber, String whiteName, String blackName,
String reason, String result){
int resultCode;
if ("1-0".equals(result))
resultCode = Game.WHITE_WINS;
else if ("0-1".equals(result))
resultCode = Game.BLACK_WINS;
else if ("1/2-1/2".equals(result))
resultCode = Game.DRAW;
else
resultCode = Game.UNKNOWN_RESULT;
closeGame(gameNumber, resultCode);
return false;
}
/**
* Invokes <code>closeGame(int)</code>.
*/
protected boolean processStoppedObserving(int gameNumber){
closeGame(gameNumber, Game.UNKNOWN_RESULT);
return false;
}
/**
* Invokes <code>closeGame(int)</code>.
*/
protected boolean processStoppedExamining(int gameNumber){
closeGame(gameNumber, Game.UNKNOWN_RESULT);
return false;
}
/**
* Invokes <code>illegalMoveAttempted</code>.
*/
protected boolean processIllegalMove(String moveString, String reason){
illegalMoveAttempted(moveString);
return false;
}
/**
* This method informs the user that he tried to use (observe, play etc.)
* a wild variant not supported by Jin. Please use this method when
* appropriate instead of sending your own message.
*/
protected void warnVariantUnsupported(String variantName){
Object [] messageFormatArgs = new Object[]{variantName};
String message =
I18n.get(JinFreechessConnection.class).getFormattedString("unsupportedVariantMessage", messageFormatArgs);
String [] messageLines = message.split("\n");
int maxLineLength = 0;
for (int i = 0; i < messageLines.length; i++)
if (messageLines[i].length() > maxLineLength)
maxLineLength = messageLines[i].length();
String border = TextUtilities.padStart("", '*', maxLineLength + 4);
processLine(border);
for (int i = 0; i < messageLines.length; i++)
processLine("* " + TextUtilities.padEnd(messageLines[i], ' ', maxLineLength) + " *");
processLine(border);
}
/**
* Called when a new game is starting. Responsible for creating the game on
* the client side and firing appropriate events. Returns an InternalGameData
* instance for the newly created Game.
*/
private InternalGameData startGame(GameInfoStruct gameInfo, Style12Struct boardData){
String categoryName = gameInfo.getGameCategory();
WildVariant variant = getVariant(categoryName);
if (variant == null){
warnVariantUnsupported(categoryName);
unsupportedGames.addElement(new Integer(gameInfo.getGameNumber()));
return null;
}
int gameType;
switch (boardData.getGameType()){
case Style12Struct.MY_GAME: gameType = Game.MY_GAME; break;
case Style12Struct.OBSERVED_GAME: gameType = Game.OBSERVED_GAME; break;
case Style12Struct.ISOLATED_BOARD: gameType = Game.ISOLATED_BOARD; break;
default:
throw new IllegalArgumentException("Bad game type value: "+boardData.getGameType());
}
Position initPos = new Position(variant);
initPos.setFEN(boardData.getBoardFEN());
String whiteName = boardData.getWhiteName();
String blackName = boardData.getBlackName();
int whiteTime = 1000 * gameInfo.getWhiteTime();
int blackTime = 1000 * gameInfo.getBlackTime();
int whiteInc = 1000 * gameInfo.getWhiteInc();
int blackInc = 1000 * gameInfo.getBlackInc();
int whiteRating = gameInfo.isWhiteRegistered() ? -1 : gameInfo.getWhiteRating();
int blackRating = gameInfo.isBlackRegistered() ? -1 : gameInfo.getBlackRating();
String gameID = String.valueOf(gameInfo.getGameNumber());
boolean isRated = gameInfo.isGameRated();
boolean isPlayed = boardData.isPlayedGame();
String whiteTitles = "";
String blackTitles = "";
boolean initiallyFlipped = boardData.isBoardFlipped();
Player currentPlayer = playerForString(boardData.getCurrentPlayer());
Player userPlayer = null;
if ((gameType == Game.MY_GAME) && isPlayed)
userPlayer = boardData.isMyTurn() ? currentPlayer : currentPlayer.getOpponent();
Game game = new Game(gameType, initPos, boardData.getPlayedPlyCount(), whiteName, blackName,
whiteTime, whiteInc, blackTime, blackInc, whiteRating, blackRating, gameID, categoryName,
isRated, isPlayed, whiteTitles, blackTitles, initiallyFlipped, userPlayer);
InternalGameData gameData = new InternalGameData(game);
ongoingGamesData.put(new Integer(gameInfo.getGameNumber()), gameData);
listenerManager.fireGameEvent(new GameStartEvent(this, game));
// The server doesn't send us seek remove lines during games, so we have
// no choice but to remove *all* seeks during a game. The seeks are restored
// when a game ends by setting seekinfo to 1 again.
if (gameType == Game.MY_GAME)
removeAllSeeks();
return gameData;
}
/**
* Updates any game parameters that differ in the board data from the current
* game data.
*/
private void updateGame(InternalGameData gameData, Style12Struct boardData){
Game game = gameData.game;
Style12Struct oldBoardData = gameData.boardData;
updateClocks(gameData, boardData); // Update the clocks
// Flip board
if ((oldBoardData != null) && (oldBoardData.isBoardFlipped() != boardData.isBoardFlipped()))
flipBoard(gameData, boardData);
game.setWhiteName(boardData.getWhiteName()); // Change white name
game.setBlackName(boardData.getBlackName()); // Change black name
game.setWhiteTime(1000 * boardData.getInitialTime()); // Change white's initial time
game.setWhiteInc(1000 * boardData.getIncrement()); // Change white's increment
game.setBlackTime(1000 * boardData.getInitialTime()); // Change black's initial time
game.setBlackInc(1000 * boardData.getIncrement()); // Change black's increment
gameData.boardData = boardData;
}
/**
* Gets called when a move is made. Fires an appropriate MoveMadeEvent.
*/
private void makeMove(InternalGameData gameData, Style12Struct boardData){
Game game = gameData.game;
Style12Struct oldBoardData = gameData.boardData;
String moveVerbose = boardData.getMoveVerbose();
String moveSAN = boardData.getMoveSAN();
WildVariant variant = game.getVariant();
Position position = new Position(variant);
position.setLexigraphic(oldBoardData.getBoardLexigraphic());
Player currentPlayer = playerForString(oldBoardData.getCurrentPlayer());
position.setCurrentPlayer(currentPlayer);
Move move;
Square fromSquare, toSquare;
Piece promotionPiece = null;
if (moveVerbose.equals("o-o"))
move = variant.createShortCastling(position);
else if (moveVerbose.equals("o-o-o"))
move = variant.createLongCastling(position);
else{
fromSquare = Square.parseSquare(moveVerbose.substring(2, 4));
toSquare = Square.parseSquare(moveVerbose.substring(5, 7));
int promotionCharIndex = moveVerbose.indexOf("=")+1;
if (promotionCharIndex != 0){
String pieceString = moveVerbose.substring(promotionCharIndex, promotionCharIndex + 1);
if (currentPlayer.isBlack()) // The server always sends upper case characters, even for black pieces.
pieceString = pieceString.toLowerCase();
promotionPiece = variant.parsePiece(pieceString);
}
move = variant.createMove(position, fromSquare, toSquare, promotionPiece, moveSAN);
}
listenerManager.fireGameEvent(new MoveMadeEvent(this, game, move, true));
// (isNew == true) because FICS never sends the entire move history
Vector unechoedGameMoves = (Vector)unechoedMoves.get(game);
if ((unechoedGameMoves != null) && (unechoedGameMoves.size() != 0)){ // Might be our move.
Move madeMove = (Move)unechoedGameMoves.elementAt(0);
if (isSameMove(game, move, madeMove))
unechoedGameMoves.removeElementAt(0);
}
gameData.addMove(move);
}
/**
* Returns whether <code>echoedMove</code> (sent to us by the server)
* is the same move as <code>sentMove</code> (a move we sent to the server).
*/
private static boolean isSameMove(Game game, Move echoedMove, Move sentMove){
try{
String echoedMoveString = moveToString(game, echoedMove);
String sentMoveString = moveToString(game, sentMove);
return echoedMoveString.equals(sentMoveString);
} catch (IllegalArgumentException e){
// An exception shouldn't be thrown for sentMove (since moveToString was
// already called on it when it was sent to the server). Thus if it is
// thrown, it's for echoedMove, in which case it's certainly not the
// same move.
return false;
}
}
/**
* Fires an appropriate ClockAdjustmentEvent.
*/
private void updateClocks(InternalGameData gameData, Style12Struct boardData){
Game game = gameData.game;
int whiteTime = boardData.getWhiteTime();
int blackTime = boardData.getBlackTime();
Player currentPlayer = playerForString(boardData.getCurrentPlayer());
// Don't make clocks run for an isolated position.
boolean isIsolatedBoard = game.getGameType() == Game.ISOLATED_BOARD;
boolean whiteRunning = (!isIsolatedBoard) && boardData.isClockRunning() && currentPlayer.isWhite();
boolean blackRunning = (!isIsolatedBoard) && boardData.isClockRunning() && currentPlayer.isBlack();
listenerManager.fireGameEvent(new ClockAdjustmentEvent(this, game, Player.WHITE_PLAYER, whiteTime, whiteRunning));
listenerManager.fireGameEvent(new ClockAdjustmentEvent(this, game, Player.BLACK_PLAYER, blackTime, blackRunning));
}
/**
* Fires an appropriate GameEndEvent.
*/
private void closeGame(int gameNumber, int result){
Integer gameID = new Integer(gameNumber);
if (gameID.intValue() == primaryPlayedGame)
primaryPlayedGame = -1;
else if (gameID.intValue() == primaryObservedGame)
primaryObservedGame = -1;
InternalGameData gameData = (InternalGameData)ongoingGamesData.remove(gameID);
if (gameData != null){
Game game = gameData.game;
game.setResult(result);
listenerManager.fireGameEvent(new GameEndEvent(this, game, result));
if ((game.getGameType() == Game.MY_GAME) && getIvarState(Ivar.SEEKINFO))
setIvarState(Ivar.SEEKINFO, true); // Refresh the seeks
}
else
unsupportedGames.removeElement(gameID);
}
/**
* Fires an appropriate BoardFlipEvent.
*/
private void flipBoard(InternalGameData gameData, Style12Struct newBoardData){
listenerManager.fireGameEvent(new BoardFlipEvent(this, gameData.game, newBoardData.isBoardFlipped()));
}
/**
* Fires an appropriate IllegalMoveEvent.
*/
private void illegalMoveAttempted(String moveString){
try{
InternalGameData gameData = findMyGame();
Game game = gameData.game;
Vector unechoedGameMoves = (Vector)unechoedMoves.get(game);
// Not a move we made (probably the user typed it in)
if ((unechoedGameMoves == null) || (unechoedGameMoves.size() == 0))
return;
Move move = (Move)unechoedGameMoves.elementAt(0);
// We have no choice but to allow (moveString == null) because the server
// doesn't always send us the move string (for example if it's not our turn).
if ((moveString == null) || moveToString(game, move).equals(moveString)){
// Our move, probably
unechoedGameMoves.removeAllElements();
listenerManager.fireGameEvent(new IllegalMoveEvent(this, game, move));
}
} catch (NoSuchGameException e){}
}
/**
* Determines whether it's possible to issue a takeback for the specified
* game change and if so calls issueTakeback, otherwise calls changePosition.
*/
private void tryIssueTakeback(InternalGameData gameData, Style12Struct boardData){
Style12Struct oldBoardData = gameData.boardData;
int plyDifference = oldBoardData.getPlayedPlyCount() - boardData.getPlayedPlyCount();
if ((gameData.getMoveCount() < plyDifference)) // Can't issue takeback
changePosition(gameData, boardData);
else if (gameData.isBSetup)
changePosition(gameData, boardData);
else{
Game game = gameData.game;
Vector moveList = gameData.moveList;
// Check whether the positions match, otherwise it could just be someone
// issuing "bsetup fen ..." after making a few moves which resets the ply
// count.
Position oldPos = game.getInitialPosition();
for (int i = 0; i < moveList.size() - plyDifference; i++){
Move move = (Move)moveList.elementAt(i);
oldPos.makeMove(move);
}
Position newPos = game.getInitialPosition();
newPos.setFEN(boardData.getBoardFEN());
if (newPos.equals(oldPos))
issueTakeback(gameData, boardData);
else
changePosition(gameData, boardData);
}
}
/**
* Fires an appropriate TakebackEvent.
*/
private void issueTakeback(InternalGameData gameData, Style12Struct newBoardData){
Style12Struct oldBoardData = gameData.boardData;
int takebackCount = oldBoardData.getPlayedPlyCount() - newBoardData.getPlayedPlyCount();
listenerManager.fireGameEvent(new TakebackEvent(this, gameData.game, takebackCount));
gameData.removeLastMoves(takebackCount);
}
/**
* Fires an appropriate PositionChangedEvent.
*/
private void changePosition(InternalGameData gameData, Style12Struct newBoardData){
Game game = gameData.game;
Position newPos = game.getInitialPosition();
newPos.setFEN(newBoardData.getBoardFEN());
game.setInitialPosition(newPos);
game.setPliesSinceStart(newBoardData.getPlayedPlyCount());
listenerManager.fireGameEvent(new PositionChangedEvent(this, game, newPos));
gameData.clearMoves();
// We do this because moves in bsetup mode cause position change events, not move events
if (gameData.isBSetup){
Vector unechoedGameMoves = (Vector)unechoedMoves.get(game);
if ((unechoedGameMoves != null) && (unechoedGameMoves.size() != 0))
unechoedGameMoves.removeElementAt(0);
}
}
/**
* Maps seek IDs to Seek objects currently in the sought list.
*/
private final Hashtable seeks = new Hashtable();
/**
* Returns the SeekListenerManager via which you can register and unregister
* SeekListeners.
*/
public SeekListenerManager getSeekListenerManager(){
return getFreechessListenerManager();
}
/**
* Creates an appropriate Seek object and fires a SeekEvent.
*/
protected boolean processSeekAdded(SeekInfoStruct seekInfo){
// We may get seeks after setting seekinfo to false because the server
// already sent them when we sent it the request to set seekInfo to false.
if (getRequestedIvarState(Ivar.SEEKINFO)){
WildVariant variant = getVariant(seekInfo.getMatchType());
if (variant != null){
String seekID = String.valueOf(seekInfo.getSeekIndex());
StringBuffer titlesBuf = new StringBuffer();
int titles = seekInfo.getSeekerTitles();
if ((titles & SeekInfoStruct.COMPUTER) != 0)
titlesBuf.append("(C)");
if ((titles & SeekInfoStruct.GM) != 0)
titlesBuf.append("(GM)");
if ((titles & SeekInfoStruct.IM) != 0)
titlesBuf.append("(IM)");
if ((titles & SeekInfoStruct.FM) != 0)
titlesBuf.append("(FM)");
if ((titles & SeekInfoStruct.WGM) != 0)
titlesBuf.append("(WGM)");
if ((titles & SeekInfoStruct.WIM) != 0)
titlesBuf.append("(WIM)");
if ((titles & SeekInfoStruct.WFM) != 0)
titlesBuf.append("(WFM)");
boolean isProvisional = (seekInfo.getSeekerProvShow() == 'P');
boolean isSeekerRated = (seekInfo.getSeekerRating() != 0);
boolean isRegistered = ((seekInfo.getSeekerTitles() & SeekInfoStruct.UNREGISTERED) == 0);
boolean isComputer = ((seekInfo.getSeekerTitles() & SeekInfoStruct.COMPUTER) != 0);
Player color;
switch (seekInfo.getSeekerColor()){
case 'W':
color = Player.WHITE_PLAYER;
break;
case 'B':
color = Player.BLACK_PLAYER;
break;
case '?':
color = null;
break;
default:
throw new IllegalStateException("Bad desired color char: "+seekInfo.getSeekerColor());
}
boolean isRatingLimited = ((seekInfo.getOpponentMinRating() > 0) || (seekInfo.getOpponentMaxRating() < 9999));
Seek seek = new Seek(seekID, seekInfo.getSeekerHandle(), titlesBuf.toString(), seekInfo.getSeekerRating(),
isProvisional, isRegistered, isSeekerRated, isComputer, variant, seekInfo.getMatchType(),
seekInfo.getMatchTime()*60*1000, seekInfo.getMatchIncrement()*1000, seekInfo.isMatchRated(), color,
isRatingLimited, seekInfo.getOpponentMinRating(), seekInfo.getOpponentMaxRating(),
!seekInfo.isAutomaticAccept(), seekInfo.isFormulaUsed());
Integer seekIndex = new Integer(seekInfo.getSeekIndex());
Seek oldSeek = (Seek)seeks.get(seekIndex);
if (oldSeek != null)
listenerManager.fireSeekEvent(new SeekEvent(this, SeekEvent.SEEK_REMOVED, oldSeek));
seeks.put(seekIndex, seek);
listenerManager.fireSeekEvent(new SeekEvent(this, SeekEvent.SEEK_ADDED, seek));
}
}
return true;
}
/**
* Issues the appropriate SeekEvents and removes the seeks.
*/
protected boolean processSeeksRemoved(int [] removedSeeks){
for (int i = 0; i < removedSeeks.length; i++){
Integer seekIndex = new Integer(removedSeeks[i]);
Seek seek = (Seek)seeks.get(seekIndex);
if (seek == null) // Happens if the seek is one we didn't fire an event for,
continue; // for example if we don't support the variant.
listenerManager.fireSeekEvent(new SeekEvent(this, SeekEvent.SEEK_REMOVED, seek));
seeks.remove(seekIndex);
}
return true;
}
/**
* Issues the appropriate SeeksEvents and removes the seeks.
*/
protected boolean processSeeksCleared(){
removeAllSeeks();
return true;
}
/**
* Removes all the seeks and notifies the listeners.
*/
private void removeAllSeeks(){
int seeksCount = seeks.size();
if (seeksCount != 0){
Object [] seeksIndices = new Object[seeksCount];
// Copy all the keys into a temporary array
Enumeration seekIDsEnum = seeks.keys();
for (int i = 0; i < seeksCount; i++)
seeksIndices[i] = seekIDsEnum.nextElement();
// Remove all the seeks one by one, notifying any interested listeners.
for (int i = 0; i < seeksCount; i++){
Object seekIndex = seeksIndices[i];
Seek seek = (Seek)seeks.get(seekIndex);
listenerManager.fireSeekEvent(new SeekEvent(this, SeekEvent.SEEK_REMOVED, seek));
seeks.remove(seekIndex);
}
}
}
/**
* This method is called by our FreechessJinListenerManager when a new
* SeekListener is added and we already had registered listeners (meaning that
* iv_seekinfo was already on, so we need to notify the new listeners of all
* existing seeks as well).
*/
void notFirstListenerAdded(SeekListener listener){
Enumeration seeksEnum = seeks.elements();
while (seeksEnum.hasMoreElements()){
Seek seek = (Seek)seeksEnum.nextElement();
SeekEvent evt = new SeekEvent(this, SeekEvent.SEEK_ADDED, seek);
listener.seekAdded(evt);
}
}
/**
* This method is called by our ChessclubJinListenerManager when the last
* SeekListener is removed.
*/
void lastSeekListenerRemoved(){
seeks.clear();
}
/**
* Maps offer indices to the <code>InternalGameData</code> objects
* representing the games in which the offer was made.
*/
private final Hashtable offerIndicesToGameData = new Hashtable();
/**
* Override processOffer to always return true, since we don't want the
* user to ever see these messages.
*/
protected boolean processOffer(boolean toUser, String offerType, int offerIndex,
String oppName, String offerParams){
super.processOffer(toUser, offerType, offerIndex, oppName, offerParams);
return true;
}
/**
* Overrides the superclass' method only to return true.
*/
protected boolean processMatchOffered(boolean toUser, int offerIndex, String oppName,
String matchDetails){
super.processMatchOffered(toUser, offerIndex, oppName, matchDetails);
return true;
}
/**
* Fires the appropriate OfferEvent(s).
*/
protected boolean processTakebackOffered(boolean toUser, int offerIndex, String oppName,
int takebackCount){
super.processTakebackOffered(toUser, offerIndex, oppName, takebackCount);
try{
InternalGameData gameData = findMyGameAgainst(oppName);
Player userPlayer = gameData.game.getUserPlayer();
Player player = toUser ? userPlayer.getOpponent() : userPlayer;
offerIndicesToGameData.put(new Integer(offerIndex), gameData);
gameData.indicesToTakebackOffers.put(new Integer(offerIndex),
new Pair(player, new Integer(takebackCount)));
updateTakebackOffer(gameData, player, takebackCount);
} catch (NoSuchGameException e){}
return true;
}
/**
* Fires the appropriate OfferEvent(s).
*/
protected boolean processDrawOffered(boolean toUser, int offerIndex, String oppName){
super.processDrawOffered(toUser, offerIndex, oppName);
processOffered(toUser, offerIndex, oppName, OfferEvent.DRAW_OFFER);
return true;
}
/**
* Fires the appropriate OfferEvent(s).
*/
protected boolean processAbortOffered(boolean toUser, int offerIndex, String oppName){
super.processAbortOffered(toUser, offerIndex, oppName);
processOffered(toUser, offerIndex, oppName, OfferEvent.ABORT_OFFER);
return true;
}
/**
* Fires the appropriate OfferEvent(s).
*/
protected boolean processAdjournOffered(boolean toUser, int offerIndex, String oppName){
super.processAdjournOffered(toUser, offerIndex, oppName);
processOffered(toUser, offerIndex, oppName, OfferEvent.ADJOURN_OFFER);
return true;
}
/**
* Gets called by the various process[offerType]Offered() methods to handle
* the offers uniformly.
*/
private void processOffered(boolean toUser, int offerIndex, String oppName, int offerId){
try{
InternalGameData gameData = findMyGameAgainst(oppName);
Player userPlayer = gameData.game.getUserPlayer();
Player player = toUser ? userPlayer.getOpponent() : userPlayer;
offerIndicesToGameData.put(new Integer(offerIndex), gameData);
gameData.indicesToOffers.put(new Integer(offerIndex),
new Pair(player, new Integer(offerId)));
updateOffers(gameData, offerId, player, true);
} catch (NoSuchGameException e){}
}
/**
* Fires the appropriate OfferEvent(s).
*/
protected boolean processOfferRemoved(int offerIndex){
super.processOfferRemoved(offerIndex);
InternalGameData gameData =
(InternalGameData)offerIndicesToGameData.remove(new Integer(offerIndex));
if (gameData != null){
// Check regular offers
Pair offer = (Pair)gameData.indicesToOffers.remove(new Integer(offerIndex));
if (offer != null){
Player player = (Player)offer.getFirst();
int offerId = ((Integer)offer.getSecond()).intValue();
updateOffers(gameData, offerId, player, false);
}
else{
// Check takeback offers
offer = (Pair)gameData.indicesToTakebackOffers.remove(new Integer(offerIndex));
if (offer != null){
Player player = (Player)offer.getFirst();
updateTakebackOffer(gameData, player, 0);
}
}
}
return true;
}
/**
* Fires the appropriate OfferEvent(s).
*/
protected boolean processPlayerCounteredTakebackOffer(int gameNum, String playerName,
int takebackCount){
super.processPlayerCounteredTakebackOffer(gameNum, playerName, takebackCount);
try{
InternalGameData gameData = getGameData(gameNum);
Player player = gameData.game.getPlayerNamed(playerName);
updateTakebackOffer(gameData, player.getOpponent(), 0);
updateTakebackOffer(gameData, player, takebackCount);
} catch (NoSuchGameException e){}
return false;
}
/**
* Fires the appropriate OfferEvent(s).
*/
protected boolean processPlayerOffered(int gameNum, String playerName, String offerName){
super.processPlayerOffered(gameNum, playerName, offerName);
try{
InternalGameData gameData = getGameData(gameNum);
Player player = gameData.game.getPlayerNamed(playerName);
int offerId;
try{
offerId = offerIdForOfferName(offerName);
updateOffers(gameData, offerId, player, true);
} catch (IllegalArgumentException e){}
} catch (NoSuchGameException e){}
return false;
}
/**
* Fires the appropriate OfferEvent(s).
*/
protected boolean processPlayerDeclined(int gameNum, String playerName, String offerName){
super.processPlayerDeclined(gameNum, playerName, offerName);
try{
InternalGameData gameData = getGameData(gameNum);
Player player = gameData.game.getPlayerNamed(playerName);
int offerId;
try{
offerId = offerIdForOfferName(offerName);
updateOffers(gameData, offerId, player.getOpponent(), false);
} catch (IllegalArgumentException e){}
} catch (NoSuchGameException e){}
return false;
}
/**
* Fires the appropriate OfferEvent(s).
*/
protected boolean processPlayerWithdrew(int gameNum, String playerName, String offerName){
super.processPlayerWithdrew(gameNum, playerName, offerName);
try{
InternalGameData gameData = getGameData(gameNum);
Player player = gameData.game.getPlayerNamed(playerName);
int offerId;
try{
offerId = offerIdForOfferName(offerName);
updateOffers(gameData, offerId, player, false);
} catch (IllegalArgumentException e){}
} catch (NoSuchGameException e){}
return false;
}
/**
* Fires the appropriate OfferEvent(s).
*/
protected boolean processPlayerOfferedTakeback(int gameNum, String playerName, int takebackCount){
super.processPlayerOfferedTakeback(gameNum, playerName, takebackCount);
try{
InternalGameData gameData = getGameData(gameNum);
Player player = gameData.game.getPlayerNamed(playerName);
updateTakebackOffer(gameData, player, takebackCount);
} catch (NoSuchGameException e){}
return false;
}
/**
* Returns the offerId (as defined by OfferEvent) corresponding to the
* specified offer name. Throws an IllegalArgumentException if the offer name
* is not recognizes.
*/
private static int offerIdForOfferName(String offerName) throws IllegalArgumentException{
if ("draw".equals(offerName))
return OfferEvent.DRAW_OFFER;
else if ("abort".equals(offerName))
return OfferEvent.ABORT_OFFER;
else if ("adjourn".equals(offerName))
return OfferEvent.ADJOURN_OFFER;
else if ("takeback".equals(offerName))
return OfferEvent.TAKEBACK_OFFER;
else
throw new IllegalArgumentException("Unknown offer name: "+offerName);
}
/**
* Updates the specified offer, firing any necessary events.
*/
private void updateOffers(InternalGameData gameData, int offerId, Player player, boolean on){
Game game = gameData.game;
if (offerId == OfferEvent.TAKEBACK_OFFER){
// We're forced to fake this so that an event is fired even if we start observing a game
// with an existing takeback offer (of which we're not aware).
if ((!on) && (gameData.getTakebackOffer(player) == 0))
gameData.setTakebackOffer(player, 1);
updateTakebackOffer(gameData, player.getOpponent(), 0); // Remove any existing offers
updateTakebackOffer(gameData, player, on ? 1 : 0);
// 1 as the server doesn't tell us how many
}
else{// if (gameData.isOffered(offerId, player) != on){ this
// We check this because we might get such an event if we start observing a game with
// an existing offer.
gameData.setOffer(offerId, player, on);
listenerManager.fireGameEvent(new OfferEvent(this, game, offerId, on, player));
}
}
/**
* Updates the takeback offer in the specified game to the specified amount of
* plies.
*/
private void updateTakebackOffer(InternalGameData gameData, Player player, int takebackCount){
Game game = gameData.game;
int oldTakeback = gameData.getTakebackOffer(player);
if (oldTakeback != 0)
listenerManager.fireGameEvent(new OfferEvent(this, game, false, player, oldTakeback));
gameData.setTakebackOffer(player, takebackCount);
if (takebackCount != 0)
listenerManager.fireGameEvent(new OfferEvent(this, game, true, player, takebackCount));
}
/**
* Accepts the given seek. Note that the given seek must be an instance generated
* by this SeekJinConnection and it must be in the current sought list.
*/
public void acceptSeek(Seek seek){
if (!seeks.contains(seek))
throw new IllegalArgumentException("The specified seek is not on the seek list");
sendCommand("$play "+seek.getID());
}
/**
* Issues the specified seek.
*/
public void issueSeek(UserSeek seek){
WildVariant variant = seek.getVariant();
String wildName = getWildName(variant);
if (wildName == null)
throw new IllegalArgumentException("Unsupported variant: " + variant);
Player color = seek.getColor();
String seekCommand = "$seek " + seek.getTime() + " " + seek.getInc() + " " +
(seek.isRated() ? "rated" : "unrated") + " " +
(color == null ? "" : color.isWhite() ? "white " : "black ") +
wildName + " " +
(seek.isManualAccept() ? "manual " : "") +
(seek.isFormula() ? "formula " : "") +
(seek.getMinRating() == Integer.MIN_VALUE ? "0" : String.valueOf(seek.getMinRating())) + "-" +
(seek.getMaxRating() == Integer.MAX_VALUE ? "9999" : String.valueOf(seek.getMaxRating())) + " ";
sendCommand(seekCommand);
}
/**
* Sends the "exit" command to the server.
*/
public void exit(){
sendCommand("$quit");
}
/**
* Quits the specified game.
*/
public void quitGame(Game game){
Object id = game.getID();
switch (game.getGameType()){
case Game.MY_GAME:
if (game.isPlayed())
sendCommand("$resign");
else
sendCommand("$unexamine");
break;
case Game.OBSERVED_GAME:
sendCommand("$unobserve "+id);
break;
case Game.ISOLATED_BOARD:
break;
}
}
/**
* Makes the given move in the given game.
*/
public void makeMove(Game game, Move move){
Enumeration gamesDataEnum = ongoingGamesData.elements();
boolean ourGame = false;
while (gamesDataEnum.hasMoreElements()){
InternalGameData gameData = (InternalGameData)gamesDataEnum.nextElement();
if (gameData.game == game){
ourGame = true;
break;
}
}
if (!ourGame)
throw new IllegalArgumentException("The specified Game object was not created by this JinConnection or the game has ended.");
sendCommand(moveToString(game, move));
Vector unechoedGameMoves = (Vector)unechoedMoves.get(game);
if (unechoedGameMoves == null){
unechoedGameMoves = new Vector(2);
unechoedMoves.put(game, unechoedGameMoves);
}
unechoedGameMoves.addElement(move);
}
/**
* Converts the given move into a string we can send to the server.
* Throws an <code>IllegalArgumentException</code> if the move is not of a
* type that we know how to send to the server.
*/
private static String moveToString(Game game, Move move) throws IllegalArgumentException{
WildVariant variant = game.getVariant();
if (move instanceof ChessMove){
ChessMove cmove = (ChessMove)move;
if (cmove.isShortCastling())
return "O-O";
else if (cmove.isLongCastling())
return "O-O-O";
String s = cmove.getStartingSquare().toString() + cmove.getEndingSquare().toString();
if (cmove.isPromotion())
return s + "=" + variant.pieceToString(cmove.getPromotionTarget());
else
return s;
}
else
throw new IllegalArgumentException("Unsupported Move type: "+move.getClass());
}
/**
* Resigns the given game. The given game must be a played game and of type
* Game.MY_GAME.
*/
public void resign(Game game){
checkGameMineAndPlayed(game);
sendCommand("$resign");
}
/**
* Sends a request to draw the given game. The given game must be a played
* game and of type Game.MY_GAME.
*/
public void requestDraw(Game game){
checkGameMineAndPlayed(game);
sendCommand("$draw");
}
/**
* Returns <code>true</code>.
*/
public boolean isAbortSupported(){
return true;
}
/**
* Sends a request to abort the given game. The given game must be a played
* game and of type Game.MY_GAME.
*/
public void requestAbort(Game game){
checkGameMineAndPlayed(game);
sendCommand("$abort");
}
/**
* Returns <code>true</code>.
*/
public boolean isAdjournSupported(){
return true;
}
/**
* Sends a request to adjourn the given game. The given game must be a played
* game and of type Game.MY_GAME.
*/
public void requestAdjourn(Game game){
checkGameMineAndPlayed(game);
sendCommand("$adjourn");
}
/**
* Returns <code>true</code>.
*/
public boolean isTakebackSupported(){
return true;
}
/**
* Sends "takeback 1" to the server.
*/
public void requestTakeback(Game game){
checkGameMineAndPlayed(game);
sendCommand("$takeback 1");
}
/**
* Returns <code>true</code>.
*/
public boolean isMultipleTakebackSupported(){
return true;
}
/**
* Sends "takeback plyCount" to the server.
*/
public void requestTakeback(Game game, int plyCount){
checkGameMineAndPlayed(game);
if (plyCount < 1)
throw new IllegalArgumentException("Illegal ply count: " + plyCount);
sendCommand("$takeback " + plyCount);
}
/**
* Goes back the given amount of plies in the given game. If the given amount
* of plies is bigger than the amount of plies since the beginning of the game,
* goes to the beginning of the game.
*/
public void goBackward(Game game, int plyCount){
checkGameMineAndExamined(game);
if (plyCount < 1)
throw new IllegalArgumentException("Illegal ply count: " + plyCount);
sendCommand("$backward " + plyCount);
}
/**
* Goes forward the given amount of plies in the given game. If the given amount
* of plies is bigger than the amount of plies remaining until the end of the
* game, goes to the end of the game.
*/
public void goForward(Game game, int plyCount){
checkGameMineAndExamined(game);
if (plyCount < 1)
throw new IllegalArgumentException("Illegal ply count: " + plyCount);
sendCommand("$forward " + plyCount);
}
/**
* Goes to the beginning of the given game.
*/
public void goToBeginning(Game game){
checkGameMineAndExamined(game);
sendCommand("$backward 999");
}
/**
* Goes to the end of the given game.
*/
public void goToEnd(Game game){
checkGameMineAndExamined(game);
sendCommand("$forward 999");
}
/**
* Throws an IllegalArgumentException if the given Game is not of type
* Game.MY_GAME or is not a played game. Otherwise, simply returns.
*/
private void checkGameMineAndPlayed(Game game){
if ((game.getGameType() != Game.MY_GAME) || (!game.isPlayed()))
throw new IllegalArgumentException("The given game must be of type Game.MY_GAME and a played one");
}
/**
* Throws an IllegalArgumentException if the given Game is not of type
* Game.MY_GAME or is a played game. Otherwise, simply returns.
*/
private void checkGameMineAndExamined(Game game){
if ((game.getGameType() != Game.MY_GAME)||game.isPlayed())
throw new IllegalArgumentException("The given game must be of type Game.MY_GAME and an examined one");
}
/**
* Sends the "help" command to the server.
*/
public void showServerHelp(){
sendCommand("$help");
}
/**
* Sends the specified question string to channel 1.
*/
public void sendHelpQuestion(String question){
sendCommand("$tell 1 [" + Jin.getInstance().getAppName() + " " + Jin.getInstance().getAppVersion() + "] "+ question);
}
/**
* Overrides ChessclubConnection.execRunnable(Runnable) to execute the
* runnable on the AWT thread using SwingUtilities.invokeLater(Runnable),
* since this class is meant to be used by Jin, a graphical interface using
* Swing.
*
* @see ChessclubConnection#execRunnable(Runnable)
* @see SwingUtilities.invokeLater(Runnable)
*/
public void execRunnable(Runnable runnable){
SwingUtilities.invokeLater(runnable);
}
}
|