package me.desht.chesscraft.chess.ai;
import chesspresso.Chess;
import me.desht.chesscraft.ChessCraft;
import me.desht.chesscraft.Messages;
import me.desht.chesscraft.chess.ChessGame;
import me.desht.chesscraft.chess.TimeControl;
import me.desht.chesscraft.chess.player.ChessPlayer;
import me.desht.dhutils.Debugger;
import me.desht.dhutils.LogUtils;
import org.bukkit.Bukkit;
import org.bukkit.ChatColor;
import org.bukkit.configuration.ConfigurationSection;
import org.bukkit.scheduler.BukkitTask;
import java.util.List;
/**
* @author desht
*
*/
public abstract class ChessAI implements Runnable {
/*
* Special character ensures AI name cannot (easily) be faked/hacked, also
* adds another level of AI name visibility. Users/admins should NOT be given
* control of this prefix - use something else to enable changing AI name
* colors, if wanted.
*/
public static final String AI_PREFIX = ChatColor.WHITE.toString();
public enum PendingAction { NONE, MOVED, DRAW_OFFERED, DRAW_ACCEPTED, DRAW_DECLINED }
private boolean active = false;
private BukkitTask aiTask;
private boolean hasFailed = false;
private PendingAction pendingAction = PendingAction.NONE;
private int pendingFrom, pendingTo;
private boolean ready = false;
private boolean drawOffered = false; // draw offered *to* the AI
private final String name;
private final ChessGame chessCraftGame;
private final boolean isWhite;
protected final ConfigurationSection params;
protected final String gameDetails;
ChessAI(String name, ChessGame chessCraftGame, Boolean isWhite, ConfigurationSection params) {
this.name = name;
this.chessCraftGame = chessCraftGame;
this.isWhite = isWhite;
this.params = params;
this.gameDetails = "game [" + chessCraftGame.getName() + "] AI [" + getName() + "]: ";
}
/**
* Perform the implementation-specfic steps needed to cleanly shuto down this AI instance.
*/
public abstract void shutdown();
/* (non-Javadoc)
* @see java.lang.Runnable#run()
* Called when the AI has to calculate its next move.
*/
public abstract void run();
/**
* Perform the implementation-specfic steps needed to undo the AI's last move.
*/
public abstract void undoLastMove();
public abstract void notifyTimeControl(TimeControl timeControl);
/**
* Perform the implementation-specfic steps needed to update the AI's internal game model with
* the given move. Square indices are always in Chesspresso sqi format.
*
* @param fromSqi Square being moved from
* @param toSqi Square being move to
* @param otherPlayer true if this is the other player moving, false if it's us
*/
protected abstract void movePiece(int fromSqi, int toSqi, boolean otherPlayer);
/**
* Offer a draw to the AI. The default implementation just rejects any offers, but subclasses may
* override this if the implementing AI supports being offered a draw.
*/
public void offerDraw() {
rejectDrawOffer();
}
/**
* Get the AI's canonical name. This is dependent only on the internal prefix.
*
* @return
*/
public String getName() {
return ChessAI.AI_PREFIX + name;
}
/**
* Get the AI's displayed name. This may vary depending on the "ai.name_format" config setting.
*
* @return
*/
public String getDisplayName() {
String fmt = ChessCraft.getInstance().getConfig().getString("ai.name_format", "[AI]<NAME>").replace("<NAME>", name);
return ChessAI.AI_PREFIX + fmt + ChatColor.RESET;
}
public ChessGame getChessCraftGame() {
return chessCraftGame;
}
protected boolean isDrawOfferedToAI() {
return drawOffered;
}
protected void setDrawOfferedToAI(boolean drawOffered) {
this.drawOffered = drawOffered;
}
public boolean isWhite() {
return isWhite;
}
public PendingAction getPendingAction() {
return pendingAction;
}
public void clearPendingAction() {
pendingAction = PendingAction.NONE;
}
public int getPendingFrom() {
return pendingFrom;
}
public int getPendingTo() {
return pendingTo;
}
public boolean hasFailed() {
return hasFailed;
}
public void setFailed(boolean failed) {
hasFailed = failed;
}
protected void setReady() {
ready = true;
}
public boolean isReady() {
return ready;
}
/**
* Check if it's the AI's move. Note this does not necessarily mean the AI is actively thinking
* right now, just that it's the AI's move.
*
* @return
*/
public boolean toMove() {
int toMove = getChessCraftGame().getPosition().getToPlay();
return isWhite && toMove == Chess.WHITE || !isWhite && toMove == Chess.BLACK;
}
/**
* Delete a running AI instance. Called when a game is finished, deleted, or the plugin is disabled.
*/
public void delete() {
setActive(false);
AIFactory.getInstance().deleteAI(this);
shutdown();
}
/**
* Set the AI-active state. Will cause either the launch or termination of the AI calculation thread.
*
* @param active
*/
public void setActive(boolean active) {
if (active == this.active)
return;
this.active = active;
Debugger.getInstance().debug(gameDetails + "active => " + active);
if (active) {
startThinking();
} else {
stopThinking();
}
}
/**
* Inform the AI that the other player has made the given move. We are assuming the move is legal,
* since it's already been validated by Chesspresso in the ChessGame object. This also sets this AI to active,
* so it starts calculating the next move.
*
* @param fromSqi the square the other player has moved from
* @param toSqi the square the other player has moved to
*/
public void userHasMoved(int fromSqi, int toSqi) {
if (active) {
LogUtils.warning(gameDetails + "userHasMoved() called while AI is active?");
return;
}
try {
movePiece(fromSqi, toSqi, true);
Debugger.getInstance().debug(gameDetails + "userHasMoved: " + fromSqi + "->" + toSqi);
} catch (Exception e) {
// oops
aiHasFailed(e);
}
setActive(true);
}
/**
* Replay a list of Chesspresso moves into the AI object. Called when a game is restored
* from persisted data.
*
* @param moves
*/
public void replayMoves(List<Short> moves) {
active = isWhite;
for (short move : moves) {
int from = chesspresso.move.Move.getFromSqi(move);
int to = chesspresso.move.Move.getToSqi(move);
movePiece(from, to, !active);
active = !active;
}
Debugger.getInstance().debug(gameDetails + "ChessAI: replayed " + moves.size() + " moves: AI to move = " + active);
if (active) {
startThinking();
}
}
/**
* Tell the AI to start thinking. This will call a run() method, implemented in subclasses,
* which will analyze the current board position and culminate by calling aiHasMoved() with the
* AI's next move.
*/
private void startThinking() {
long delay = ChessCraft.getInstance().getConfig().getInt("ai.min_move_wait", 0);
aiTask = Bukkit.getScheduler().runTaskLaterAsynchronously(ChessCraft.getInstance(), this, delay * 20L);
}
/**
* Tell the AI to stop thinking.
*/
private void stopThinking() {
if (Bukkit.getScheduler().isCurrentlyRunning(aiTask.getTaskId())) {
Debugger.getInstance().debug(gameDetails + "forcing shutdown for AI task #" + aiTask);
aiTask.cancel();
}
aiTask = null;
}
/**
* Called when the AI has come up with its next move. Square indices always use the
* Chesspresso sqi representation.
*
* @param fromSqi the square the AI is moving from
* @param toSqi the square the AI is moving to.
*/
protected void aiHasMoved(int fromSqi, int toSqi) {
if (!active) {
LogUtils.warning(gameDetails + "aiHasMoved() called when AI not active?");
return;
}
if (isDrawOfferedToAI()) {
// making a move effectively rejects any pending draw offer
rejectDrawOffer();
}
setActive(false);
movePiece(fromSqi, toSqi, false);
Debugger.getInstance().debug(gameDetails + "aiHasMoved: " + fromSqi + "->" + toSqi);
// Moving directly isn't thread-safe: we'd end up altering the Minecraft world from a separate thread,
// which is Very Bad. So we just note the move made now, and let the ChessGame object check for it on
// the next clock tick.
pendingFrom = fromSqi;
pendingTo = toSqi;
pendingAction = PendingAction.MOVED;
}
protected void makeDrawOffer() {
pendingAction = PendingAction.DRAW_OFFERED;
}
protected void acceptDrawOffer() {
pendingAction = PendingAction.DRAW_ACCEPTED;
}
protected void rejectDrawOffer() {
pendingAction = PendingAction.DRAW_DECLINED;
}
public ChessPlayer getChessPlayer() {
int colour = isWhite ? Chess.WHITE : Chess.BLACK;
return getChessCraftGame().getPlayer(colour);
}
public ChessPlayer getOtherChessPlayer() {
int colour = isWhite ? Chess.BLACK : Chess.WHITE;
return getChessCraftGame().getPlayer(colour);
}
/**
* Something has gone horribly wrong. Need to abandon this game.
*
* @param e
*/
protected void aiHasFailed(Exception e) {
LogUtils.severe(gameDetails + "Unexpected Exception in AI");
e.printStackTrace();
chessCraftGame.alert(Messages.getString("ChessAI.AIunexpectedException", e.getMessage())); //$NON-NLS-1$
hasFailed = true;
}
public static boolean isAIPlayer(String playerName) {
return playerName.startsWith(AI_PREFIX);
}
}