package me.desht.chesscraft.chess;
import chesspresso.Chess;
import chesspresso.game.Game;
import chesspresso.move.IllegalMoveException;
import chesspresso.move.Move;
import chesspresso.pgn.PGN;
import chesspresso.pgn.PGNWriter;
import chesspresso.position.Position;
import com.google.common.collect.Lists;
import me.desht.chesscraft.*;
import me.desht.chesscraft.chess.ai.AIFactory;
import me.desht.chesscraft.chess.ai.ChessAI;
import me.desht.chesscraft.chess.player.AIChessPlayer;
import me.desht.chesscraft.chess.player.ChessPlayer;
import me.desht.chesscraft.chess.player.HumanChessPlayer;
import me.desht.chesscraft.enums.GameResult;
import me.desht.chesscraft.enums.GameState;
import me.desht.chesscraft.event.ChessGameStateChangedEvent;
import me.desht.chesscraft.exceptions.ChessException;
import me.desht.chesscraft.results.Results;
import me.desht.chesscraft.util.ChessUtils;
import me.desht.chesscraft.util.EconomyUtil;
import me.desht.dhutils.*;
import org.bukkit.Bukkit;
import org.bukkit.ChatColor;
import org.bukkit.command.CommandSender;
import org.bukkit.command.ConsoleCommandSender;
import org.bukkit.configuration.Configuration;
import org.bukkit.configuration.ConfigurationSection;
import org.bukkit.configuration.MemoryConfiguration;
import org.bukkit.configuration.serialization.ConfigurationSerializable;
import org.bukkit.entity.Player;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.*;
import java.util.Map.Entry;
public class ChessGame implements ConfigurationSerializable, ChessPersistable {
private final String name;
private final Game cpGame;
private final long created;
private final ChessPlayer[] players = new ChessPlayer[2];
private final List<Short> history = new ArrayList<Short>();
private UUID invited;
private GameState state;
private long started, finished, lastMoved, lastOpenInvite;
private int result;
private double stake;
private boolean openInvite;
private final List<GameListener> listeners = Lists.newArrayList();
private final TwoPlayerClock clock;
/**
* Create a new Chess game.
*
* @param name Name of the game
* @param creator Name of the player who is setting up the game
* @param tcSpec The time control to use for the game
* @param colour Colour the game creator will play (Chess.WHITE or Chess.BLACK)
* @throws ChessException If the game can't be created for any reason
*/
public ChessGame(String name, Player creator, String tcSpec, int colour) {
this.name = name;
players[Chess.WHITE] = players[Chess.BLACK] = null;
if (creator != null) {
players[colour] = createPlayer(creator.getUniqueId().toString(), creator.getDisplayName(), colour);
}
state = GameState.SETTING_UP;
invited = null;
openInvite = false;
created = System.currentTimeMillis();
started = finished = lastOpenInvite = 0L;
result = Chess.RES_NOT_FINISHED;
stake = 0.0;
clock = new TwoPlayerClock(tcSpec);
cpGame = setupChesspressoGame();
// view.setGame(this);
}
/**
* Constructor: Restoring a saved Chess game.
*
* @param conf Saved game data
* @throws ChessException If the game can't be created for any reason
* @throws IllegalMoveException If the game data contains an illegal Chess move
*/
public ChessGame(ConfigurationSection conf) throws IllegalMoveException {
name = conf.getString("name");
String dispW = conf.getString("playerWhiteDisp", "?white?");
String dispB = conf.getString("playerBlackDisp", "?black?");
players[Chess.WHITE] = createPlayer(conf.getString("playerWhite"), dispW, Chess.WHITE);
players[Chess.BLACK] = createPlayer(conf.getString("playerBlack"), dispB, Chess.BLACK);
state = GameState.valueOf(conf.getString("state"));
String inv = conf.getString("invited", "");
invited = inv.isEmpty() ? null : UUID.fromString(inv);
openInvite = conf.getBoolean("openInvite", false);
List<Integer> hTmp = conf.getIntegerList("moves");
for (int m : hTmp) {
history.add((short) m);
}
this.clock = (TwoPlayerClock) conf.get("clock");
created = conf.getLong("created", System.currentTimeMillis());
started = conf.getLong("started");
finished = conf.getLong("finished", state == GameState.FINISHED ? System.currentTimeMillis() : 0);
lastOpenInvite = 0L;
lastMoved = conf.getLong("lastMoved", System.currentTimeMillis());
result = conf.getInt("result");
if (hasPlayer(Chess.WHITE)) getPlayer(Chess.WHITE).setPromotionPiece(conf.getInt("promotionWhite"));
if (hasPlayer(Chess.BLACK)) getPlayer(Chess.BLACK).setPromotionPiece(conf.getInt("promotionBlack"));
stake = conf.getDouble("stake", 0.0);
cpGame = setupChesspressoGame();
replayMoves();
if (getState() == GameState.RUNNING) {
clock.setActivePlayer(getPosition().getToPlay());
getPlayerToMove().promptForNextMove();
}
}
private ChessPlayer createPlayer(String playerId, String displayName, int colour) {
if (playerId == null) {
String aiName = AIFactory.getInstance().getFreeAIName();
return new AIChessPlayer(aiName, this, colour);
} else if (ChessAI.isAIPlayer(playerId)) {
return new AIChessPlayer(playerId, this, colour);
} else if (playerId.isEmpty()) {
// no player for this slot yet
return null;
} else {
return new HumanChessPlayer(playerId, displayName, this, colour);
}
}
public Map<String, Object> serialize() {
Map<String, Object> map = new HashMap<String, Object>();
map.put("name", name);
map.put("playerWhite", getPlayerId(Chess.WHITE));
map.put("playerBlack", getPlayerId(Chess.BLACK));
map.put("playerWhiteDisp", getPlayerDisplayName(Chess.WHITE));
map.put("playerBlackDisp", getPlayerDisplayName(Chess.BLACK));
map.put("state", state.toString());
map.put("invited", invited);
map.put("openInvite", openInvite);
map.put("moves", history);
map.put("created", created);
map.put("started", started);
map.put("finished", finished);
map.put("lastMoved", lastMoved);
map.put("result", result);
map.put("promotionWhite", getPromotionPiece(Chess.WHITE));
map.put("promotionBlack", getPromotionPiece(Chess.BLACK));
map.put("clock", clock);
map.put("stake", stake);
return map;
}
public static ChessGame deserialize(Map <String,Object> map) throws IllegalMoveException {
Configuration conf = new MemoryConfiguration();
for (Entry<String, Object> e : map.entrySet()) {
conf.set(e.getKey(), e.getValue());
}
return new ChessGame(conf);
}
@Override
public String getName() {
return name;
}
@Override
public File getSaveDirectory() {
return DirectoryStructure.getGamesPersistDirectory();
}
/**
* Replay the move history to restore the saved board position. We do this
* instead of just saving the position so that the Chesspresso ChessGame model
* includes a history of the moves, suitable for creating a PGN file.
*
* @throws IllegalMoveException
*/
private void replayMoves() throws IllegalMoveException {
// load moves into the Chesspresso model
for (short move : history) {
getPosition().doMove(move);
}
// load moves into the player's (possibly AI) game model
if (players[Chess.WHITE] != null) players[Chess.WHITE].replayMoves();
if (players[Chess.BLACK] != null) players[Chess.BLACK].replayMoves();
}
private Game setupChesspressoGame() {
Game cpg = new Game();
String site = Bukkit.getServerName() + Messages.getString("Game.sitePGN");
// seven tag roster
cpg.setTag(PGN.TAG_EVENT, getName());
cpg.setTag(PGN.TAG_SITE, site);
cpg.setTag(PGN.TAG_DATE, ChessUtils.dateToPGNDate(created));
cpg.setTag(PGN.TAG_ROUND, "?");
cpg.setTag(PGN.TAG_WHITE, getPlayerId(Chess.WHITE));
cpg.setTag(PGN.TAG_BLACK, getPlayerId(Chess.BLACK));
cpg.setTag(PGN.TAG_RESULT, getPGNResult());
// extra tags
cpg.setTag(PGN.TAG_FEN, Position.createInitialPosition().getFEN());
return cpg;
}
public void save() {
ChessCraft.getInstance().getPersistenceHandler().savePersistable("game", this);
}
public Game getChesspressoGame() {
return cpGame;
}
public Position getPosition() {
return cpGame.getPosition();
}
/**
* Return the player object for the given colour (Chess.WHITE or Chess.BLACK).
*
* @param colour the colour, Chess.WHITE or Chess.BLACK
* @return the chess player object
*/
public ChessPlayer getPlayer(int colour) {
return players[colour];
}
public boolean hasPlayer(int colour) {
return players[colour] != null;
}
public ChessPlayer getPlayer(String playerId) {
if (playerId.equals(getPlayerId(Chess.WHITE))) {
return getPlayer(Chess.WHITE);
} else if (playerId.equals(getPlayerId(Chess.BLACK))) {
return getPlayer(Chess.BLACK);
} else {
return null;
}
}
public String getPlayerId(int colour) {
return players[colour] != null ? players[colour].getId() : "";
}
public String getPlayerDisplayName(int colour) {
return players[colour] != null ? players[colour].getDisplayName() : "";
}
public UUID getInvitedId() {
return invited;
}
public GameState getState() {
return state;
}
public void setState(GameState state) {
if (state == GameState.RUNNING) {
ChessValidate.isTrue(this.state == GameState.SETTING_UP, "invalid state transition " + this.state + "->" + state);
started = lastMoved = System.currentTimeMillis();
clock.setActivePlayer(Chess.WHITE);
} else if (state == GameState.FINISHED) {
ChessValidate.isTrue(this.state == GameState.RUNNING, "invalid state transition " + this.state + "->" + state);
finished = System.currentTimeMillis();
clock.stop();
}
this.state = state;
Bukkit.getPluginManager().callEvent(new ChessGameStateChangedEvent(this));
for (GameListener l : listeners) {
l.gameStateChanged(state);
}
}
public long getStarted() {
return started;
}
public long getCreated() {
return created;
}
public long getFinished() {
return finished;
}
public List<Short> getHistory() {
return history;
}
public double getStake() {
return stake;
}
public int getPromotionPiece(int colour) {
return hasPlayer(colour) ? getPlayer(colour).getPromotionPiece() : Chess.QUEEN;
}
public boolean isOpenInvite() {
return openInvite;
}
/**
* A player is trying to adjust the stake for this game.
*
* @param player player setting the stake
* @param newStake The stake being set
* @throws ChessException if the stake is out of range or not affordable or the game isn't in setup phase
*/
public void setStake(Player player, double newStake) {
if (EconomyUtil.enabled()) {
ensureGameState(GameState.SETTING_UP);
ensurePlayerInGame(player.getUniqueId().toString());
for (GameListener l : listeners) {
ChessValidate.isTrue(l.tryStakeChange(newStake), Messages.getString("Game.stakeLocked"));
}
ChessValidate.isTrue(newStake >= 0.0, Messages.getString("Game.noNegativeStakes"));
ChessValidate.isTrue(EconomyUtil.has(player, newStake), Messages.getString("ChessCommandExecutor.cantAffordStake"));
double max = ChessCraft.getInstance().getConfig().getDouble("stake.max");
ChessValidate.isTrue(max < 0.0 || newStake <= max, Messages.getString("Game.stakeTooHigh", max));
ChessValidate.isFalse(isFull(), Messages.getString("Game.stakeCantBeChanged"));
this.stake = newStake;
for (GameListener l : listeners) {
l.stakeChanged(newStake);
}
}
}
/**
* Adjust the game's stake by the given amount.
*
* @param player player adjusting the stake
* @param adjustment amount to adjust by (may be negative)
* @throws ChessException if the new stake is out of range or not affordable or the game isn't in setup phase
*/
public void adjustStake(Player player, double adjustment) {
if (!EconomyUtil.enabled()) {
return;
}
double newStake = getStake() + adjustment;
double max = ChessCraft.getInstance().getConfig().getDouble("stake.max");
if (max >= 0.0 && newStake > max && adjustment < 0.0) {
// allow stake to be adjusted down without throwing an exception
// could happen if global max stake was changed to something lower than
// a game's current stake setting
newStake = Math.min(max, EconomyUtil.getBalance(player));
}
if (!EconomyUtil.has(player, newStake) && adjustment < 0.0) {
// similarly for the player's own balance
newStake = Math.min(max, EconomyUtil.getBalance(player));
}
setStake(player, newStake);
}
public TimeControl getTimeControl() {
return new TimeControl(clock.getTimeControl().getSpec());
}
/**
* Housekeeping task, called periodically by the scheduler. Update the clocks for the game, and
* check for any pending AI moves.
*/
public void tick() {
if (state == GameState.RUNNING) {
checkForAIActivity();
}
checkForAutoDelete();
}
public void setTimeControl(String tcSpec) {
ensureGameState(GameState.SETTING_UP);
for (GameListener l : listeners) {
ChessValidate.isTrue(l.tryTimeControlChange(tcSpec), Messages.getString("Game.timeControlLocked"));
}
clock.setTimeControl(tcSpec);
for (GameListener l : listeners) {
l.timeControlChanged(tcSpec);
}
}
public void swapColours() {
tick();
ChessPlayer tmp = players[Chess.WHITE];
players[Chess.WHITE] = players[Chess.BLACK];
players[Chess.BLACK] = tmp;
players[Chess.WHITE].setColour(Chess.WHITE);
players[Chess.BLACK].setColour(Chess.BLACK);
players[Chess.WHITE].alert(Messages.getString("Game.nowPlayingWhite"));
players[Chess.BLACK].alert(Messages.getString("Game.nowPlayingBlack"));
}
/**
* Check if the game is full, i.e. has a player for both colours.
*
* @return true if the game is full, false otherwise
*/
public boolean isFull() {
return hasPlayer(Chess.WHITE) && hasPlayer(Chess.BLACK);
}
/**
* Add the named player (which could be an AI) to the game.
*
* @param playerId ID of the player to add
* @param displayName display name of the player to add
* @return the colour the player was added as
* @throws ChessException if the player could not be added for any reason
*/
public int addPlayer(String playerId, String displayName) {
ensureGameState(GameState.SETTING_UP);
ChessValidate.isFalse(isFull(), Messages.getString("Game.gameIsFull"));
ChessPlayer cp = fillEmptyPlayerSlot(playerId, displayName);
clearInvitation();
for (GameListener l : listeners) {
l.playerAdded(cp);
}
if (isFull()) {
if (ChessCraft.getInstance().getConfig().getBoolean("autostart", true)) {
start(playerId);
} else {
alert(Messages.getString("Game.startPrompt"));
}
}
return cp.getColour();
}
/**
* Add the given player (human or AI) to the first empty slot (white or black, in order)
* found in the game.
*
* @param playerId id of the player to add
* @param displayName display name of the player to add
* @return the colour the player was added as
* @throws ChessException if the player may not join for any reason
*/
private ChessPlayer fillEmptyPlayerSlot(String playerId, String displayName) {
int colour = hasPlayer(Chess.WHITE) ? Chess.BLACK : Chess.WHITE;
ChessPlayer chessPlayer = createPlayer(playerId, displayName, colour);
chessPlayer.validateInvited("Game.notInvited");
chessPlayer.validateAffordability("Game.cantAffordToJoin");
players[colour] = chessPlayer;
int otherColour = Chess.otherPlayer(colour);
if (hasPlayer(otherColour)) {
getPlayer(otherColour).alert(Messages.getString("Game.playerJoined", chessPlayer.getDisplayName()));
}
return chessPlayer;
}
/**
* One player has just invited another player to this game.
*
* @param inviter player doing the inviting
* @param inviteeName name of the invited player (could be an AI)
* @throws ChessException
*/
public void invitePlayer(Player inviter, String inviteeName) {
inviteSanityCheck(inviter.getUniqueId().toString());
if (inviteeName == null) {
inviteOpen(inviter);
return;
}
// Getting player by name is actually OK here - we don't want players to have to type
// player UUID's just to invite them!
@SuppressWarnings("deprecation") Player invitee = Bukkit.getServer().getPlayer(inviteeName);
if (invitee != null) {
alert(invitee, Messages.getString("Game.youAreInvited", inviter.getDisplayName()));
if (EconomyUtil.enabled() && getStake() > 0.0) {
alert(invitee, Messages.getString("Game.gameHasStake", EconomyUtil.formatStakeStr(getStake())));
}
alert(invitee, Messages.getString("Game.joinPrompt"));
if (invited != null) {
alert(invited, Messages.getString("Game.inviteWithdrawn"));
}
invited = invitee.getUniqueId();
openInvite = false;
alert(inviter, Messages.getString("Game.inviteSent", invitee.getDisplayName()));
} else {
// no human by this name, try to add an AI of the given name
addPlayer(ChessAI.AI_PREFIX + inviteeName, ChessAI.AI_PREFIX + inviteeName);
}
}
/**
* Broadcast an open invitation to the server and allow anyone to join the game.
*
* @param inviter player creating the open invitation
*/
public void inviteOpen(Player inviter) {
inviteSanityCheck(inviter.getUniqueId().toString());
long now = System.currentTimeMillis();
Duration cooldown = new Duration(ChessCraft.getInstance().getConfig().getString("open_invite_cooldown", "3 mins"));
long remaining = (cooldown.getTotalDuration() - (now - lastOpenInvite)) / 1000;
if (remaining > 0) {
throw new ChessException(Messages.getString("Game.inviteCooldown", remaining));
}
MiscUtil.broadcastMessage((Messages.getString("Game.openInviteCreated", inviter.getDisplayName())));
if (EconomyUtil.enabled() && getStake() > 0.0) {
MiscUtil.broadcastMessage(Messages.getString("Game.gameHasStake", EconomyUtil.formatStakeStr(getStake())));
}
MiscUtil.broadcastMessage(Messages.getString("Game.joinPromptGlobal", getName()));
openInvite = true;
lastOpenInvite = now;
}
private void inviteSanityCheck(String inviterId) {
ensurePlayerInGame(inviterId);
ensureGameState(GameState.SETTING_UP);
if (isFull()) {
// if one player is an AI, allow the AI to leave
if (!players[Chess.WHITE].isHuman()) {
players[Chess.WHITE].cleanup();
players[Chess.WHITE] = null;
} else if (!players[Chess.BLACK].isHuman()) {
players[Chess.BLACK].cleanup();
players[Chess.BLACK] = null;
} else {
throw new ChessException(Messages.getString("Game.gameIsFull"));
}
}
}
/**
* Withdraw any invitation to this game.
*/
public void clearInvitation() {
invited = null;
openInvite = false;
}
/**
* Start the game.
*
* @param playerId Player who is starting the game
* @throws ChessException if anything goes wrong
*/
public void start(String playerId) {
ensurePlayerInGame(playerId);
ensureGameState(GameState.SETTING_UP);
if (!isFull()) {
// game started with only one player - add an AI player
fillEmptyPlayerSlot(null, null);
}
cpGame.setTag(PGN.TAG_WHITE, getPlayerDisplayName(Chess.WHITE));
cpGame.setTag(PGN.TAG_BLACK, getPlayerDisplayName(Chess.BLACK));
if (stake > 0.0 && !getPlayerId(Chess.WHITE).equals(getPlayerId(Chess.BLACK))) {
// just in case stake.max got adjusted after game creation...
double max = ChessCraft.getInstance().getConfig().getDouble("stake.max");
if (max >= 0 && stake > max) {
stake = max;
}
getPlayer(Chess.WHITE).validateAffordability("Game.cantAffordToStart");
getPlayer(Chess.BLACK).validateAffordability("Game.cantAffordToStart");
getPlayer(Chess.WHITE).withdrawFunds(stake);
getPlayer(Chess.BLACK).withdrawFunds(stake);
}
clearInvitation();
setState(GameState.RUNNING);
getPlayer(Chess.WHITE).promptForFirstMove();
save();
}
/**
* The given player is resigning.
*
* @param colour colour of the resigning player
*/
public void resign(int colour) {
ChessValidate.isTrue(getState() == GameState.RUNNING, Messages.getString("Game.notStarted"));
ChessPlayer cp = getPlayer(colour);
ChessValidate.notNull(cp, Messages.getString("Game.notInGame"));
gameOver(Chess.otherPlayer(cp.getColour()), GameResult.Resigned);
}
/**
* Player has won by default (other player has exhausted their time control,
* or has been offline too long).
*
* @param colour colour of the winning player
*/
public void winByDefault(int colour) {
ChessValidate.isTrue(getState() == GameState.RUNNING, Messages.getString("Game.notStarted"));
ChessPlayer cp = getPlayer(colour);
ChessValidate.notNull(cp, Messages.getString("Game.notInGame"));
gameOver(cp.getColour(), GameResult.Forfeited);
}
/**
* The game is a draw.
*
* @param res the reason for the draw
*/
public void drawn(GameResult res) {
ensureGameState(GameState.RUNNING);
gameOver(Chess.NOBODY, res);
}
/**
* Do a move for player from fromSquare to toSquare.
*
* @param playerId ID of the player who is moving
* @param fromSquare sqi of the square being moved from
* @param toSquare sqi of the square being moved to
* @throws IllegalMoveException
* @throws ChessException
*/
public void doMove(String playerId, int fromSquare, int toSquare) throws IllegalMoveException, ChessException {
ensureGameState(GameState.RUNNING);
ensurePlayerToMove(playerId);
if (fromSquare == Chess.NO_SQUARE) {
return;
}
boolean isCapturing = getPosition().getPiece(toSquare) != Chess.NO_PIECE;
short move = Move.getRegularMove(fromSquare, toSquare, isCapturing);
short realMove = validateMove(move);
int prevToMove = getPosition().getToPlay();
// At this point we know the move is valid, so go ahead and make the necessary changes...
getPosition().doMove(realMove); // the board view will be repainted at this point
lastMoved = System.currentTimeMillis();
history.add(realMove);
getPlayer(prevToMove).cancelOffers();
if (!checkForFinishingPosition()) {
// the game continues...
getPlayer(getPosition().getToPlay()).promptForNextMove();
}
save();
}
/**
* Check the current game position to see if the game is over.
*
* @return true if game is over, false if not
*/
private boolean checkForFinishingPosition() {
if (getPosition().isMate()) {
gameOver(Chess.otherPlayer(getPosition().getToPlay()), GameResult.Checkmate);
return true;
} else if (getPosition().isStaleMate()) {
gameOver(Chess.NOBODY, GameResult.Stalemate);
return true;
} else if (getPosition().getHalfMoveClock() >= 50) {
gameOver(Chess.NOBODY, GameResult.FiftyMoveRule);
return true;
}
return false;
}
/**
* Check if the move is really allowed. Also account for special cases:
* castling, en passant, pawn promotion
*
* @param move move to check
* @return move, if allowed
* @throws IllegalMoveException if not allowed
*/
private short validateMove(short move) throws IllegalMoveException {
int sqiFrom = Move.getFromSqi(move);
int sqiTo = Move.getToSqi(move);
int toPlay = getPosition().getToPlay();
if (getPosition().getPiece(sqiFrom) == Chess.KING) {
// Castling?
if (sqiFrom == Chess.E1 && sqiTo == Chess.G1 || sqiFrom == Chess.E8 && sqiTo == Chess.G8) {
move = Move.getShortCastle(toPlay);
} else if (sqiFrom == Chess.E1 && sqiTo == Chess.C1 || sqiFrom == Chess.E8 && sqiTo == Chess.C8) {
move = Move.getLongCastle(toPlay);
}
} else if (getPosition().getPiece(sqiFrom) == Chess.PAWN
&& (Chess.sqiToRow(sqiTo) == 7 || Chess.sqiToRow(sqiTo) == 0)) {
// Promotion?
boolean capturing = getPosition().getPiece(sqiTo) != Chess.NO_PIECE;
move = Move.getPawnMove(sqiFrom, sqiTo, capturing, getPromotionPiece(toPlay));
} else if (getPosition().getPiece(sqiFrom) == Chess.PAWN && getPosition().getPiece(sqiTo) == Chess.NO_PIECE) {
// En passant?
int toCol = Chess.sqiToCol(sqiTo);
int fromCol = Chess.sqiToCol(sqiFrom);
if ((toCol == fromCol - 1 || toCol == fromCol + 1)
&& (Chess.sqiToRow(sqiFrom) == 4 && Chess.sqiToRow(sqiTo) == 5 || Chess.sqiToRow(sqiFrom) == 3
&& Chess.sqiToRow(sqiTo) == 2)) {
move = Move.getEPMove(sqiFrom, sqiTo);
}
}
for (short aMove : getPosition().getAllMoves()) {
if (move == aMove) {
return move;
}
}
throw new IllegalMoveException(move);
}
/**
* Given a move in SAN format, try to find the Chesspresso Move for that SAN move in the
* current position.
*
* @param moveSAN the move in SAN format
* @return the Move object, or null if not found (i.e the supplied move was not legal)
*/
public Move getMoveFromSAN(String moveSAN) {
Position tempPos = new Position(getPosition().getFEN());
for (short aMove : getPosition().getAllMoves()) {
try {
tempPos.doMove(aMove);
Move m = tempPos.getLastMove();
Debugger.getInstance().debug(2, "getMoveFromSAN: check supplied move: " + moveSAN + " vs possible: " + m.getSAN());
if (moveSAN.equals(m.getSAN()))
return m;
tempPos.undoMove();
} catch (IllegalMoveException e) {
// shouldn't happen
return null;
}
}
return null;
}
public String getPGNResult() {
switch (result) {
case Chess.RES_NOT_FINISHED:
return "*";
case Chess.RES_WHITE_WINS:
return "1-0";
case Chess.RES_BLACK_WINS:
return "0-1";
case Chess.RES_DRAW:
return "1/2-1/2";
default:
return "*";
}
}
private void gameOver(int winnerColour, GameResult rt) {
String p1, p2;
setState(GameState.FINISHED);
if (winnerColour == Chess.NOBODY) {
p1 = getPlayer(Chess.WHITE).getDisplayName();
p2 = getPlayer(Chess.BLACK).getDisplayName();
result = Chess.RES_DRAW;
} else {
getPlayer(winnerColour).playEffect("game_won");
getPlayer(Chess.otherPlayer(winnerColour)).playEffect("game_lost");
p1 = getPlayer(winnerColour).getDisplayName();
p2 = getPlayer(Chess.otherPlayer(winnerColour)).getDisplayName();
result = winnerColour == Chess.WHITE ? Chess.RES_WHITE_WINS : Chess.RES_BLACK_WINS;
}
cpGame.setTag(PGN.TAG_RESULT, getPGNResult());
String msg = Messages.getString(rt.getMsgKey(), p1, p2);
if (p1.equals(p2)) {
getPlayer(Chess.WHITE).alert(msg);
} else {
if (ChessCraft.getInstance().getConfig().getBoolean("broadcast_results")) {
MiscUtil.broadcastMessage(msg);
} else {
alert(msg);
}
handlePayout();
Results handler = Results.getResultsHandler();
if (handler != null) {
handler.logResult(this, rt);
}
}
}
private void handlePayout() {
if (stake <= 0.0 || getPlayerId(Chess.WHITE).equals(getPlayerId(Chess.BLACK)) || getState() == GameState.SETTING_UP) {
return;
}
switch (result) {
case Chess.RES_WHITE_WINS:
winner(Chess.WHITE);
break;
case Chess.RES_BLACK_WINS:
winner(Chess.BLACK);
break;
case Chess.RES_DRAW:
case Chess.RES_NOT_FINISHED:
players[Chess.WHITE].depositFunds(stake);
players[Chess.BLACK].depositFunds(stake);
alert(Messages.getString("Game.getStakeBack", EconomyUtil.formatStakeStr(stake)));
break;
}
stake = 0.0;
}
private void winner(int winningColour) {
int losingColour = Chess.otherPlayer(winningColour);
double winnings = stake * getPlayer(losingColour).getPayoutMultiplier();
players[winningColour].depositFunds(winnings);
players[winningColour].alert(Messages.getString("Game.youWon", EconomyUtil.formatStakeStr(winnings)));
players[losingColour].alert(Messages.getString("Game.lostStake", EconomyUtil.formatStakeStr(stake)));
}
/**
* Called when a game is deleted; handle any cleanup tasks.
*/
void onDeleted(boolean permanent) {
System.out.println("delete game " + getName() + " perm = " + permanent);
if (permanent) {
handlePayout();
for (GameListener l : listeners) {
l.gameDeleted();
}
}
if (players[Chess.WHITE] != null) {
players[Chess.WHITE].cleanup();
}
if (players[Chess.BLACK] != null) {
players[Chess.BLACK].cleanup();
}
}
/**
* Get the colour for the given player name.
*
* @param playerId ID of the player to check
* @return one of Chess.WHITE, Chess.BLACK or Chess.NOBODY
*/
public int getPlayerColour(String playerId) {
if (playerId.equalsIgnoreCase(getPlayerId(Chess.WHITE))) {
return Chess.WHITE;
} else if (playerId.equalsIgnoreCase(getPlayerId(Chess.BLACK))) {
return Chess.BLACK;
} else {
return Chess.NOBODY;
}
}
public void alert(Player player, String message) {
MiscUtil.alertMessage(player, Messages.getString("Game.alertPrefix", getName()) + message);
}
public void alert(UUID id, String message) {
Player p = Bukkit.getServer().getPlayer(id);
if (p != null) {
alert(p, message);
}
}
public void alert(String message) {
if (hasPlayer(Chess.WHITE)) {
getPlayer(Chess.WHITE).alert(message);
}
if (hasPlayer(Chess.BLACK) && !getPlayerId(Chess.WHITE).equals(getPlayerId(Chess.BLACK))) {
getPlayer(Chess.BLACK).alert(message);
}
}
public ChessPlayer getPlayerToMove() {
return getPlayer(getPosition().getToPlay());
}
public boolean isPlayerInGame(String playerId) {
return (playerId.equals(getPlayerId(Chess.WHITE)) || playerId.equals(getPlayerId(Chess.BLACK)));
}
public boolean isPlayerToMove(String playerId) {
return playerId.equals(getPlayerToMove().getId());
}
/**
* Write a PGN file for the game.
*
* @param force if true, overwrite any existing PGN file for this game
* @return the file that has been written
*/
public File writePGN(boolean force) {
File f = makePGNFile();
if (f.exists() && !force) {
throw new ChessException(Messages.getString("Game.archiveExists", f.getName()));
}
try {
PrintWriter pw = new PrintWriter(f);
PGNWriter w = new PGNWriter(pw);
w.write(cpGame.getModel());
pw.close();
return f;
} catch (FileNotFoundException e) {
throw new ChessException(Messages.getString("Game.cantWriteArchive", f.getName(), e.getMessage()));
}
}
public String getPGN() {
StringWriter strw = new StringWriter();
PGNWriter w = new PGNWriter(strw);
w.write(cpGame.getModel());
return strw.toString();
}
private File makePGNFile() {
String baseName = getName() + "_" + ChessUtils.dateToPGNDate(System.currentTimeMillis());
int n = 1;
File f;
do {
f = new File(DirectoryStructure.getPGNDirectory(), baseName + "_" + n + ".pgn");
++n;
} while (f.exists());
return f;
}
/**
* Force the board position to the given FEN string. This should only be used for testing
* since the move history for the game is invalidated.
*
* @param fen the FEN string
*/
public void setPositionFEN(String fen) {
getPosition().set(new Position(fen));
// manually overriding the position invalidates the move history
getHistory().clear();
}
/**
* Check if a game needs to be auto-deleted:
* - ChessGame that has not been started after a certain duration
* - ChessGame that has been finished for a certain duration
* - ChessGame that has been running without any moves made for a certain duration
*/
private void checkForAutoDelete() {
String alertStr = null;
long now = System.currentTimeMillis();
if (getState() == GameState.SETTING_UP) {
long elapsed = now - created;
Duration timeout = new Duration(ChessCraft.getInstance().getConfig().getString("auto_delete.not_started", "3 mins"));
if (timeout.getTotalDuration() > 0 && elapsed > timeout.getTotalDuration() && !isFull()) {
alertStr = Messages.getString("Game.autoDeleteNotStarted", timeout);
}
} else if (getState() == GameState.FINISHED) {
long elapsed = now - finished;
Duration timeout = new Duration(ChessCraft.getInstance().getConfig().getString("auto_delete.finished", "30 sec"));
if (timeout.getTotalDuration() > 0 && elapsed > timeout.getTotalDuration()) {
alertStr = Messages.getString("Game.autoDeleteFinished");
}
} else if (getState() == GameState.RUNNING) {
long elapsed = now - lastMoved;
Duration timeout = new Duration(ChessCraft.getInstance().getConfig().getString("auto_delete.running", "28 days"));
if (timeout.getTotalDuration() > 0 && elapsed > timeout.getTotalDuration()) {
alertStr = Messages.getString("Game.autoDeleteRunning", timeout);
}
}
if (alertStr != null) {
alert(alertStr);
LogUtils.info(alertStr);
ChessGameManager.getManager().deleteGame(getName(), true);
}
}
/**
* Validate that the given player is in this game.
*
* @param playerId ID of player to check for
*/
public void ensurePlayerInGame(String playerId) {
if (!playerId.equals(getPlayerId(Chess.WHITE)) && !playerId.equals(getPlayerId(Chess.BLACK))) {
throw new ChessException(Messages.getString("Game.notInGame"));
}
}
/**
* Validate that it's the given player's move in this game.
*
* @param playerId the player ID to check
*/
public void ensurePlayerToMove(String playerId) {
ChessPlayer cp = getPlayerToMove();
ChessValidate.isTrue(cp != null && playerId.equals(cp.getId()), Messages.getString("Game.notYourTurn"));
}
/**
* Validate that this game is in the given state.
*
* @param state the state to check for
*/
public void ensureGameState(GameState state) {
ChessValidate.isTrue(getState() == state, Messages.getString("Game.shouldBeState", state));
}
/**
* Check if the given player is allowed to delete this game
*
* @param sender the command sender (console or player)
* @return true if the player is allowed to delete the game, false otherwise
*/
public boolean playerCanDelete(CommandSender sender) {
if (sender instanceof ConsoleCommandSender) {
return true;
} else if (sender instanceof Player) {
if (getState() != GameState.SETTING_UP) {
return false;
}
Player player = (Player) sender;
String playerId = player.getUniqueId().toString();
String playerWhite = getPlayerId(Chess.WHITE);
String playerBlack = getPlayerId(Chess.BLACK);
if (!playerWhite.isEmpty() && playerBlack.isEmpty()) {
return playerWhite.equals(playerId);
} else if (playerWhite.isEmpty() && !playerBlack.isEmpty()) {
return playerBlack.equals(playerId);
} else if (playerWhite.equals(playerId)) {
Player other = sender.getServer().getPlayer(UUID.fromString(playerBlack));
return other == null || !other.isOnline();
} else if (playerBlack.equals(playerId)) {
Player other = sender.getServer().getPlayer(UUID.fromString(playerWhite));
return other == null || !other.isOnline();
}
return false;
} else {
return false;
}
}
/**
* Have the given player offer a draw.
*
* @param playerId ID of the player making the offer
* @throws ChessException
*/
public void offerDraw(String playerId) {
ensureGameState(GameState.RUNNING);
ChessPlayer cp = getPlayer(playerId);
if (cp == null) {
return;
}
ChessPlayer other = getPlayer(Chess.otherPlayer(cp.getColour()));
if (other == null) {
return;
}
cp.statusMessage(Messages.getString("ChessCommandExecutor.drawOfferedYou", other.getDisplayName()));
other.drawOffered();
}
/**
* Have the given player offer to swap sides.
*
* @param playerId ID of the player making the offer (could be human or AI)
* @throws ChessException
*/
public void offerSwap(String playerId) {
ChessPlayer cp = getPlayer(playerId);
if (cp != null) {
ChessPlayer other = getPlayer(Chess.otherPlayer(cp.getColour()));
if (other == null) {
// no other player yet - just swap
swapColours();
} else {
cp.statusMessage(Messages.getString("ChessCommandExecutor.swapOfferedYou", other.getDisplayName()));
other.swapOffered();
}
}
}
/**
* Have the given player offer to undo the last move they made.
*
* @param playerId Name of the player making the offer
* @throws ChessException
*/
public void offerUndoMove(String playerId) {
ChessPlayer cp = getPlayer(playerId);
if (cp == null) {
return;
}
ChessPlayer other = getPlayer(Chess.otherPlayer(cp.getColour()));
if (other == null) {
return;
}
if (other.isHuman()) {
// playing another human - we need to ask them if it's OK to undo
other.undoOffered();
cp.statusMessage(Messages.getString("ChessCommandExecutor.undoOfferedYou", other.getDisplayName()));
} else {
// playing AI - only allow undo if no stake
ChessValidate.isTrue(getStake() == 0.0, Messages.getString("ChessCommandExecutor.undoAIWithStake"));
undoMove(playerId);
}
}
/**
* Undo the most recent moves until it's the turn of the given player again. Not
* supported for AI vs AI games, only human vs human or human vs AI. The undoer
* must be human.
*
* @param playerId ID of player undoing the move
*/
public void undoMove(String playerId) {
ChessPlayer cp = getPlayer(playerId);
if (cp == null) {
return;
}
ChessPlayer other = getPlayer(Chess.otherPlayer(cp.getColour()));
if (other == null) {
return;
}
if (history.size() == 0 || history.size() == 1 && cp.getColour() == Chess.BLACK) {
// first move for the player, can't undo yet
return;
}
if (getPosition().getToPlay() == cp.getColour()) {
// need to undo two moves - first the other player's last move
other.undoLastMove();
cpGame.getPosition().undoMove();
history.remove(history.size() - 1);
}
// now undo the undoer's last move
cp.undoLastMove();
cpGame.getPosition().undoMove();
history.remove(history.size() - 1);
int toPlay = getPosition().getToPlay();
getClock().setActivePlayer(toPlay);
save();
alert(Messages.getString("Game.moveUndone", ChessUtils.getDisplayColour(toPlay)));
}
/**
* Return detailed information about the game.
*
* @return a string list of game information
*/
public List<String> getGameDetail() {
List<String> res = new ArrayList<String>();
String white = players[Chess.WHITE] == null ? "?" : players[Chess.WHITE].getDisplayName();
String black = players[Chess.BLACK] == null ? "?" : players[Chess.BLACK].getDisplayName();
String bullet = MessagePager.BULLET + ChatColor.YELLOW;
res.add(Messages.getString("ChessCommandExecutor.gameDetail.name", getName(), getState()));
res.add(bullet + Messages.getString("ChessCommandExecutor.gameDetail.players", white, black, "?")); // TODO
res.add(bullet + Messages.getString("ChessCommandExecutor.gameDetail.halfMoves", getHistory().size()));
if (EconomyUtil.enabled()) {
res.add(bullet + Messages.getString("ChessCommandExecutor.gameDetail.stake", EconomyUtil.formatStakeStr(getStake())));
}
res.add(bullet + (getPosition().getToPlay() == Chess.WHITE ?
Messages.getString("ChessCommandExecutor.gameDetail.whiteToPlay") :
Messages.getString("ChessCommandExecutor.gameDetail.blackToPlay")));
res.add(bullet + Messages.getString("ChessCommandExecutor.gameDetail.timeControlType", getClock().getTimeControl().toString()));
if (getState() == GameState.RUNNING) {
res.add(bullet + Messages.getString("ChessCommandExecutor.gameDetail.clock",
getClock().getClockString(Chess.WHITE), getClock().getClockString(Chess.BLACK)));
}
if (getInvitedId() != null) {
if (isOpenInvite()) {
res.add(bullet + Messages.getString("ChessCommandExecutor.gameDetail.openInvitation"));
} else {
Player p = Bukkit.getPlayer(getInvitedId());
if (p == null) {
invited = null;
} else {
res.add(bullet + Messages.getString("ChessCommandExecutor.gameDetail.invitation", p.getDisplayName()));
}
}
}
res.add(Messages.getString("ChessCommandExecutor.gameDetail.moveHistory"));
List<Short> h = getHistory();
StringBuilder sb = new StringBuilder();
for (int i = 0; i < h.size(); i += 2) {
sb.append(ChatColor.WHITE).append(Integer.toString((i / 2) + 1)).append(". ");
sb.append(ChatColor.YELLOW).append(Move.getString(h.get(i)));
if (i < h.size() - 1) {
sb.append(" ").append(Move.getString(h.get(i + 1)));
}
sb.append(" ");
}
res.add(sb.toString());
return res;
}
/**
* If it's been noted that the AI has moved in its game model, make the actual
* move in our game model too. Also check if the AI has failed and we need to abandon.
*/
private synchronized void checkForAIActivity() {
getPlayer(Chess.WHITE).checkPendingAction();
getPlayer(Chess.BLACK).checkPendingAction();
}
/**
* Called when a (human) player has logged out.
*
* @param playerId ID of player who left
*/
public void playerLeft(String playerId) {
int colour = getPlayerColour(playerId);
if (hasPlayer(colour)) {
getPlayer(colour).cleanup();
}
}
/**
* Update an old-style player name with a UUID.
*
* @param colour player's colour
* @param oldStyleName player's old name
* @param uuid player's UUID
*/
public void migratePlayer(int colour, String oldStyleName, UUID uuid) {
players[colour] = new HumanChessPlayer(uuid.toString(), oldStyleName, this, colour);
LogUtils.info("migrated player " + oldStyleName + " to " + uuid + " in game " + getName());
}
public void addGameListener(GameListener listener) {
listeners.add(listener);
}
public TwoPlayerClock getClock() {
return clock;
}
public List<GameListener> getListeners() {
return listeners;
}
}