package pdp.scrabble.game.impl;
import java.util.TreeSet;
import java.util.SortedSet;
import java.util.HashSet;
import java.util.ArrayList;
import java.util.List;
import java.util.Iterator;
import javax.swing.JFrame;
import pdp.scrabble.Game;
import pdp.scrabble.game.AIConfig;
import pdp.scrabble.game.SearchLevel;
import pdp.scrabble.game.SearchStrategy;
import pdp.scrabble.game.Dictionary;
import pdp.scrabble.game.Bag;
import pdp.scrabble.game.Rack;
import pdp.scrabble.game.Letter;
import pdp.scrabble.game.BoardCase;
import pdp.scrabble.game.Board;
import pdp.scrabble.game.Placement;
import pdp.scrabble.game.Player;
import pdp.scrabble.game.SearchPriority;
import pdp.scrabble.ihm.BoardPanel;
import pdp.scrabble.utility.Anagram;
import pdp.scrabble.utility.Tool;
import static pdp.scrabble.Factory.FACTORY;
import static pdp.scrabble.game.BoardCaseState.FREE;
import static pdp.scrabble.game.BoardCaseState.OLD;
import static pdp.scrabble.game.Board.HORI_DIM;
import static pdp.scrabble.game.Board.VERT_DIM;
public class SearchPlacementImpl extends Thread {
private SearchPriority priority = null;
private SearchStrategy strategy = null;
private SearchLevel level = null;
private Dictionary dico = null;
private Bag bag = null;
/** Board reference (cloned). */
private Board board = null;
/** Player reference (cloned). */
private Player player = null;
/** Original player reference. */
private Player playerOrigin = null;
/** Best placement found. */
private Placement bestPlacement = null;
/** List of required letters. */
private List<Letter> required = null;
/** Number of required letters. */
private int requiredNum = 0;
/** List of excluded letters. */
private List<Letter> excluded = null;
/** Number of excluded letters. */
private int excludedNum = 0;
/** Maximal window search. */
private int maximalWindowSize = 0;
/** Number of anagram to skip. */
private int anagramSkip = 0;
/** Min search score. */
private int scoreMin = 0;
/** Max search score. */
private int scoreMax = 0;
/** Search axis. */
private boolean vertical = false;
/** Debug board. */
private BoardPanel debug = null;
/** Debug frame. */
private JFrame frame = null;
/** Active state for debug view (show search steps). */
private boolean debugActive = false;
/** First placement state, true when it is the first (first turn). */
private boolean firstPlacement = false;
/** List of best placements. */
private SortedSet<Placement> bestPlacements = null;
/** Last placement id. */
private int placementID = 0;
/** Contain anagram on specified length. */
private static class AnagramLength {
private List<String> anag = null;
AnagramLength() {
this.anag = new ArrayList<String>(16);
}
public void add(String s) {
this.anag.add(s);
}
public Iterator<String> get() {
return this.anag.iterator();
}
}
/** List of all anagram length. */
private static List<AnagramLength> anagrams = null;
/** Build anagram length list. */
public static void initAnagrams() {
anagrams = new ArrayList<AnagramLength>(Rack.MAX_RACK_LETTERS);
Anagram anagram = new Anagram("0123456");
for (int i = 0; i < Rack.MAX_RACK_LETTERS; i++) {
anagrams.add(new AnagramLength());
}
Iterator<String> itr = anagram.get();
while (itr.hasNext()) {
String s = itr.next();
int i = s.length() - 1;
if (i > -1) {
anagrams.get(i).add(s);
}
}
}
/** Create a new placement search.
* @param game game reference.
* @param player original player reference (will be cloned).
* @param config search configuration.
* @param required list of required letters for this search.
* @param excluded list of excluded letters for this search.
* @param vertical search axis (true for vertical, false for horizontal).
*/
public SearchPlacementImpl(
Game game, Player player, AIConfig config, List<Letter> required,
List<Letter> excluded, boolean vertical) {
super("Search placement");
this.dico = game.dictionary();
this.bag = game.bag();
this.board = game.board().clone();
this.player = player.clone();
this.playerOrigin = player;
this.vertical = vertical;
this.required = required;
this.excluded = excluded;
this.bestPlacement = FACTORY.createPlacement();
this.bestPlacements = new TreeSet<Placement>();
this.firstPlacement = false;
this.placementID = 0;
// Setup configuration
this.level = config.getLevel();
this.strategy = config.getStrategy();
this.priority = config.getPrioriry();
// In case of automatic strategy
if (this.strategy == SearchStrategy.AUTOMATIC) {
int score = player.getScore();
Iterator<Player> itr = game.getPlayersList();
while (itr.hasNext()) {
Player p = itr.next();
if (p != player) {
if (score >= p.getScore()) {
this.strategy = SearchStrategy.DEFENSIVE;
}
else {
this.strategy = SearchStrategy.OFFENSIVE;
}
}
}
}
// Apply configuration
this.maximalWindowSize = 15;
this.anagramSkip = 0;
this.scoreMin = -1;
this.scoreMax = -1;
if (this.level == SearchLevel.NORMAL) {
this.maximalWindowSize = 6;
this.anagramSkip = 2;
this.scoreMin = 14;
this.scoreMax = 28;
}
else if (this.level == SearchLevel.EASY) {
this.maximalWindowSize = 4;
this.anagramSkip = 4;
this.scoreMin = 4;
this.scoreMax = 14;
}
if (required != null) {
this.requiredNum = this.required.size();
}
else {
this.requiredNum = 0;
}
if (excluded != null) {
this.excludedNum = this.excluded.size();
}
else {
this.excludedNum = 0;
}
if (player.showDebug()) {
this.debugActive = true;
}
if (this.debugActive) {
if (vertical) {
this.frame = new JFrame("Search best vertical placement");
}
else {
this.frame = new JFrame("Search best horizontal placement");
}
this.debug = new BoardPanel(this.board);
this.frame.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
this.frame.setResizable(false);
this.frame.add(this.debug);
this.frame.pack();
this.frame.validate();
this.frame.setLocationRelativeTo(null);
if (vertical) {
this.frame.setLocation(
this.frame.getX() - this.frame.getWidth() / 2,
this.frame.getY());
}
else {
this.frame.setLocation(
this.frame.getX() + this.frame.getWidth() / 2,
this.frame.getY());
}
this.repaint();
}
}
/** Update and redraw debug screen. */
private void repaint() {
if (this.debugActive) {
this.debug.paintImmediately(0, 0, 600, 600);
Tool.pause(250);
}
}
@Override
public void run() {
Placement current = null;
if (this.debugActive) {
this.frame.setVisible(true);
}
// Vertical search ?
if (this.vertical) {
if (this.board.getCase(VERT_DIM / 2, HORI_DIM / 2).getState() == FREE) {
this.firstPlacement = true;
this.bestPlacement = this.tryAllLocations(
this.board, this.player, true, true, HORI_DIM / 2);
}
else {
this.firstPlacement = false;
for (int h = 0; h < HORI_DIM; h++) {
current = this.tryAllLocations(
this.board, this.player, false, true, h);
this.bestPlacement = this.getBestPlacement(
this.bestPlacement, current, this.priority);
}
}
}
else {
if (this.board.getCase(VERT_DIM / 2, HORI_DIM / 2).getState() == FREE) {
this.firstPlacement = true;
this.bestPlacement = this.tryAllLocations(
this.board, this.player, true, false, VERT_DIM / 2);
}
else {
this.firstPlacement = false;
for (int v = 0; v < VERT_DIM; v++) {
current = this.tryAllLocations(
this.board, this.player, false, false, v);
this.bestPlacement = this.getBestPlacement(
this.bestPlacement, current, this.priority);
}
}
}
if (this.debugActive) {
this.frame.setVisible(false);
this.frame.dispose();
}
this.board = null;
this.player = null;
}
/** Keep best placement between last best and current placement.
* @param last last best placement.
* @param cur current placement found.
* @param priority priority used to designate the best placement.
* @return best placement.
*/
private Placement getBestPlacement(
Placement last, Placement cur, SearchPriority priority) {
boolean currentIsBetter = false;
// Check first placement case
if (this.level == SearchLevel.HARD) {
if (this.firstPlacement) {
if (cur.getScore() < 50) {
cur.clearActions();
return last;
}
}
}
// Check score limit
if (this.scoreMin > -1) {
if (cur.getScore() >= this.scoreMin) {
last.clearActions();
return cur;
}
}
if (this.scoreMax > -1) {
if (cur.getScore() > this.scoreMax) {
cur.clearActions();
return last;
}
}
// Minimal rules
if (this.canUseJoker(cur)
&& this.lettersPriority(cur, this.required, this.requiredNum, true)
&& this.lettersPriority(cur, this.excluded, this.excludedNum, false)) {
// Apply priority
if (priority == SearchPriority.HIGHER_SCORE) {
if (cur.getScore() > last.getScore()) {
currentIsBetter = true;
}
}
else if (priority == SearchPriority.LOWER_SCORE) {
if (cur.getScore() > -1 && cur.getScore() < last.getScore()
|| last.getScore() == -1) {
currentIsBetter = true;
}
}
else if (priority == SearchPriority.LONGEST_WORD) {
if (cur.getLength() > last.getLength()) {
currentIsBetter = true;
}
}
else if (priority == SearchPriority.SHORTEST_WORD) {
if (cur.getLength() > -1 && cur.getLength() < last.getLength()
|| last.getLength() == -1) {
currentIsBetter = true;
}
}
// Differences between same score
if (this.level == SearchLevel.HARD) {
if (cur.getScore() == last.getScore()) {
// Keep best reliquat
if (cur.getReliquatNum() <= last.getReliquatNum()) {
// Defensive keep uncompletable word
if (this.strategy == SearchStrategy.DEFENSIVE) {
if (this.isUncompletable(cur)) {
currentIsBetter = true;
}
}
else {
currentIsBetter = true;
}
}
}
}
}
// Better ?
if (currentIsBetter) {
last.clearActions();
return cur;
}
else {
cur.clearActions();
return last;
}
}
/** Check if the joker is usefull (at least 50pts).
* @param current current placement.
* @return true if usefull, false else.
*/
private boolean canUseJoker(Placement current) {
if (this.level == SearchLevel.HARD) {
if (current.getJokersNumber() > 0 && !this.bag.isEmpty()) {
if (current.getJokersNumber() == 1) {
if (this.playerOrigin.getRack().getLetterAmount(this.bag.JOKER) == 2) {
return (current.getScore() >= 30);
}
else {
return (current.getScore() >= 50);
}
}
else {
return (current.getScore() >= 50);
}
}
// No joker, skip
else {
return true;
}
}
else {
return true;
}
}
/** Check if word is uncompletable.
* @param current current placement.
* @return true if uncompletable, false else.
*/
private boolean isUncompletable(Placement current) {
return this.dico.isLast(current.getFirstWord());
}
/** Check letter rule (required or excluded).
* @param current current placement to check.
* @param rule list of letter of the rule.
* @param number number of letters to check.
* @param defaut default state.
* @return rules result.
*/
private boolean lettersPriority(Placement current, List<Letter> rule,
int number, boolean defaut) {
if (number > 0) {
List<Letter> usedLetters = current.getLetters();
int len = usedLetters.size();
HashSet<Integer> used = new HashSet<Integer>(1);
// For each required letter
for (int i = 0; i < number; i++) {
boolean found = false;
// For each used letter
for (int j = 0; j < len; j++) {
Letter letter = usedLetters.get(j);
if (!used.contains(letter.getID())) {
char a = rule.get(i).getName();
char b = letter.getName();
// Letter found, mark index as used
if (a == b) {
found = true;
used.add(letter.getID());
break;
}
}
}
if (defaut) {
if (!found) {
used.clear();
return false;
}
}
else {
if (found) {
used.clear();
return false;
}
}
}
used.clear();
used = null;
}
return true;
}
/** Establish all available windows size, excluding non valid windows.
* @param board reference.
* @param player player which is playing.
* @param center true when center need to be used.
* @param vertical true for vertical check, false for horizontal.
* @param axis axis index (vertical or horizontal index).
* @return best placement found.
*/
private Placement tryAllLocations(Board board, Player player, boolean center,
boolean vertical, int axis) {
Placement best = FACTORY.createPlacement();
int maxWindowSize = this.maximalWindowSize;
boolean ignore = true;
// Ensure there is an adjacent old letter on current line
// Or center is free (first turn)
int boardMax = HORI_DIM;
if (vertical) {
boardMax = VERT_DIM;
}
for (int i = 0; i < boardMax; i++) {
if (vertical) {
if (this.hitOldCase(i, axis)) {
ignore = false;
break;
}
}
else {
if (this.hitOldCase(axis, i)) {
ignore = false;
break;
}
}
}
// Search best placement for each window size (word length)
// Ignore empty lines
if (!ignore || center) {
for (int window = 2; window <= maxWindowSize; window++) {
// Window is the number of letter for the word
// It include both rack + board letter
// Just the result on board is important
// Minimum window size is 2, because with one letter,
// we got a word length of 2
Placement current = this.tryWindow(
board, player, center, vertical, axis, window);
// Update best placement
if (current != null) {
best = this.getBestPlacement(best, current, this.priority);
}
}
}
return best;
}
/** Try with current window size, and all window possible locations.
* @param board board reference.
* @param player player which is playing.
* @param vertical true for vertical check, false for horizontal.
* @param axis axis index (vertical or horizontal index).
* @param windowSize current window size.
* @return
*/
private Placement tryWindow(Board board, Player player, boolean center,
boolean vertical, int axis, int windowSize) {
Placement best = FACTORY.createPlacement();
Placement current = null;
BoardCase boardCase = null;
int index = 0;
int boardMax = HORI_DIM;
if (vertical) {
boardMax = VERT_DIM;
}
// For each cases (vertical or horizontal)
for (int i = 0; i < boardMax; i++) {
// Ensure window is not too large
if (i + windowSize - 1 >= boardMax) {
break;
}
// Ensure window contains at least an old horizontal letter
boolean ignoreLarge = false, ignoreOld = true, ignoreFree = true;
boolean ignoreCenter = true;
if (center) {
if (i <= boardMax / 2 && (i + windowSize) > boardMax / 2) {
ignoreCenter = false;
}
else {
continue;
}
}
// Check each window case
for (int j = 0; j < windowSize; j++) {
index = i + j;
if (vertical) {
boardCase = board.getCase(index, axis);
}
else {
boardCase = board.getCase(axis, index);
}
// Search old and free cases
if (index < boardMax) {
if (vertical) {
if (this.hitOldCase(index, axis)) {
ignoreOld = false;
}
}
else {
if (this.hitOldCase(axis, index)) {
ignoreOld = false;
}
}
if (boardCase.getState() == FREE) {
ignoreFree = false;
}
} // Window size too large, can't place word, ignore
else {
ignoreLarge = true;
break;
}
}
// If has something to do (no ignore)
if (!ignoreLarge && (!ignoreOld || !ignoreCenter) && !ignoreFree) {
// Check each free location in window from starting coordinate
StringBuilder str = new StringBuilder("");
for (int j = 0; j < windowSize; j++) {
index = i + j;
if (vertical) {
if (board.getCase(index, axis).getState() == FREE) {
str = str.append(Tool.getCharacter(index));
}
}
else {
if (board.getCase(axis, index).getState() == FREE) {
str = str.append(Tool.getCharacter(index));
}
}
}
// Apply placement
int len = str.length();
if (len <= Rack.MAX_RACK_LETTERS) {
current = this.tryLocation(
board, str, len, player, vertical, axis);
}
else {
current = null;
}
str = null;
// Update best placement
if (current != null) {
best = this.getBestPlacement(best, current, this.priority);
}
}
}
return best;
}
/** Represents location structure of a I letter on the J board coordinate. */
private static class Loc {
private int i = 0, j = 0;
private Loc(int i, int j) {
this.i = i;
this.j = j;
}
public int getI() {
return this.i;
}
public int getJ() {
return this.j;
}
}
/** Try specified location by using all available letters in any order.
* @param location location to test (containing location as letter index).
* @param player player which is playing.
* @param vertical true for vertical check, false for horizontal.
* @param axis axis index (vertical or horizontal index).
* @param len number of letter needed.
* @param letterIDStart first letter to take in rack.
*/
private Placement tryLocation(Board board, StringBuilder location, int len,
Player player, boolean vertical, int axis) {
Placement best = FACTORY.createPlacement();
HashSet<Loc> allowed = new HashSet<Loc>(1);
Letter letter = null;
int index = 0;
int skip = 0;
int i = 0, j = 0;
// Find existing letters
for (i = 0; i < len; i++) {
j = Tool.getNumericValue(location.charAt(i));
allowed.add(new Loc(i, j));
}
// Apply all possible letter positions from rack
Iterator<String> rackItr = anagrams.get(len - 1).get();
while (rackItr.hasNext()) {
// Skip anagram (depending of the difficulty
for (skip = 0; skip < this.anagramSkip; skip++) {
if (rackItr.hasNext()) {
rackItr.next();
}
}
if (!rackItr.hasNext()) {
break;
}
// Current anagram
String ana = rackItr.next();
Placement current = FACTORY.createPlacement();
// For each available location indexs
Iterator<Loc> locations = allowed.iterator();
boolean done = false;
while (locations.hasNext()) {
Loc loc = locations.next();
i = loc.getI();
j = loc.getJ();
index = Integer.parseInt(ana.charAt(i) + "");
letter = player.getRack().getLetter(index);
// Apply location
if (vertical) {
done = this.applyLocation(
board, j, axis, letter, current, vertical);
}
else {
done = this.applyLocation(
board, axis, j, letter, current, vertical);
}
loc = null;
if (!done) {
break;
}
}
locations = null;
// Check if location found is good
if (board.validateAI() && done) {
current.set(board.getFormedWords(), board.getWordPoints(),
board.getWordLength());
current.setID(this.placementID);
this.placementID++;
this.bestPlacements.add(current);
best = this.getBestPlacement(best, current, this.priority);
this.repaint();
}
else {
current.clearActions();
current = null;
}
board.cancel(null);
}
// Clear
rackItr = null;
allowed.clear();
allowed = null;
letter = null;
return best;
}
/** Apply location on board.
* @param board board reference.
* @param v vertical location.
* @param h horizontal location.
* @param letter letter to drop.
* @param placement current placement.
* @return true if board modified, false if no change.
*/
private boolean applyLocation(Board board, int v, int h, Letter letter,
Placement placement, boolean vertical) {
if (letter != null) {
letter.setColor(this.player.getColor());
board.setCaseLetter(v, h, letter, true);
if (Tool.isReliquat(letter.getName())) {
placement.setReliquatNum(placement.getReliquatNum() + 1);
}
placement.addAction(FACTORY.createLocation(v, h), letter);
return true;
}
else {
return false;
}
}
/** Check if the current boardcase is near a multiplicator (defensive).
* @param boardCase from this board case.
* @return true if near, false else.
*/
private boolean hasMultiplicatorProximity(int v, int h, boolean vertical) {
int vCheck = 1, hCheck = 0;
if (vertical) {
vCheck = 0;
hCheck = 1;
}
if (this.hasMultiplicator(this.board.getCase(v - vCheck, h - hCheck))
|| this.hasMultiplicator(this.board.getCase(v + vCheck, h + hCheck))) {
return true;
}
return false;
}
/** Check if current case has a multiplicator (>1).
* @param boardCase board case to check
* @return true if has multiplicator, false else.
*/
private boolean hasMultiplicator(BoardCase boardCase) {
return (boardCase.getLetterMult() > 1);
}
/** Check if this coordinate hits an old case.
* @param v vertical location.
* @param horizontal location.
* @return
*/
private boolean hitOldCase(int v, int h) {
if (this.board.getCase(v - 1, h).getState() == OLD
|| this.board.getCase(v, h - 1).getState() == OLD
|| this.board.getCase(v + 1, h).getState() == OLD
|| this.board.getCase(v, h + 1).getState() == OLD) {
return true;
}
else {
return false;
}
}
/** Get best placement found.
* @return best placement found.
*/
public Placement getBestPlacement() {
return this.bestPlacement;
}
/** Get list of best placements.
* @return list of best placements.
*/
public Iterator<Placement> getBestPlacements() {
return this.bestPlacements.iterator();
}
}