Commit d326e41c authored by PurkkaKoodari's avatar PurkkaKoodari

Refactor solver, speed up scoring by 10x

parent 7bba2f63
......@@ -6,7 +6,6 @@ import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import net.pietu1998.wordbasehacker.solver.Board;
......@@ -98,12 +97,6 @@ public class BoardActivity extends Activity {
this.dialog = dialog;
}
private class TrieNode {
private TrieNode[] nodes = new TrieNode[1024];
private boolean hasChildren = false;
private String content = null;
}
@Override
protected Integer doInBackground(Void... params) {
try {
......@@ -161,64 +154,16 @@ public class BoardActivity extends Activity {
}
}
short charMap[] = new short[65536];
short charIndex = 0;
Tile[][] tiles = new Tile[10][13];
short[] tileLetters = new short[130];
for (int x = 0; x < 10; x++) {
for (int y = 0; y < 13; y++) {
char letter = rows[y].charAt(x);
int mapping = charMap[letter];
if (mapping == 0)
mapping = charMap[letter] = (short) ++charIndex;
tiles[x][y] = new Tile(tileStates[x + 10 * y], letter);
tileLetters[x + 10 * y] = (short) (mapping - 1);
}
}
game.setBoard(new Board(tiles, words));
publishProgress(R.string.analyzing_words);
words = game.getBoard().getWords();
TrieNode root = new TrieNode();
for (String word : words) {
if (game.isPlayed(word))
continue;
TrieNode node = root;
for (int i = 0; i < word.length(); i++) {
int mapping = charMap[word.charAt(i)] - 1;
node.hasChildren = true;
TrieNode next = node.nodes[mapping];
if (next == null)
node = node.nodes[mapping] = new TrieNode();
else
node = next;
}
node.content = word;
}
Board board = new Board(rows, tileStates, words, game);
publishProgress(R.string.finding_words);
results = new ArrayList<>();
long start = System.currentTimeMillis();
for (int iteration = 0; iteration < 500; iteration++) {
boolean positions[] = new boolean[130];
byte coords[] = new byte[24];
for (int y = 0, index = 0; y < 13; y++)
for (int x = 0; x < 10; x++, index++)
if ((tileStates[index] & Tile.PLAYER) != 0)
findWords(tileLetters, 0, x, y, index, coords, positions, root);
if (iteration < 499) results.clear();
}
long end = System.currentTimeMillis();
Log.d("WordbaseHacker", "Finding took " + (end - start) + "ms");
board.findWords();
publishProgress(R.string.scoring_words);
start = System.currentTimeMillis();
for (int iteration = 0; iteration < 20; iteration++)
for (Possibility pos : results)
game.getBoard().score(pos, game.isFlipped());
end = System.currentTimeMillis();
Log.d("WordbaseHacker", "Scoring took " + (end - start) + "ms");
board.scoreWords(game.isFlipped());
results = board.getResults();
return 0;
} catch (NumberFormatException | ParseException | SQLiteException | IndexOutOfBoundsException e) {
Log.e("WordbaseHacker", "Failed to read data.", e);
......@@ -226,31 +171,6 @@ public class BoardActivity extends Activity {
}
}
private void findWords(short[] tiles, int length, int x, int y, int index, byte[] coords, boolean[] positions, TrieNode node) {
if (length >= 12 || x < 0 || x > 9 || y < 0 || y > 12 || positions[index])
return;
short letter = tiles[index];
node = node.nodes[letter];
if (node == null)
return;
coords[length * 2] = (byte) x;
coords[length * 2 + 1] = (byte) y;
if (node.content != null)
results.add(new Possibility(Arrays.copyOf(coords, length * 2 + 2), node.content));
if (!node.hasChildren)
return;
positions[index] = true;
findWords(tiles, length + 1, x, y + 1, index + 10, coords, positions, node);
findWords(tiles, length + 1, x - 1, y + 1, index + 9, coords, positions, node);
findWords(tiles, length + 1, x + 1, y + 1, index + 11, coords, positions, node);
findWords(tiles, length + 1, x - 1, y, index - 1, coords, positions, node);
findWords(tiles, length + 1, x + 1, y, index + 1, coords, positions, node);
findWords(tiles, length + 1, x, y - 1, index - 10, coords, positions, node);
findWords(tiles, length + 1, x - 1, y - 1, index - 11, coords, positions, node);
findWords(tiles, length + 1, x + 1, y - 1, index - 9, coords, positions, node);
positions[index] = false;
}
@Override
protected void onProgressUpdate(Integer... values) {
dialog.setMessage(getResources().getString(values[0]));
......
......@@ -9,6 +9,7 @@ import android.graphics.Paint;
import android.graphics.PixelFormat;
import android.graphics.Typeface;
import android.graphics.drawable.Drawable;
import android.support.annotation.NonNull;
public class BoardDrawable extends Drawable {
......@@ -33,7 +34,7 @@ public class BoardDrawable extends Drawable {
}
@Override
public void draw(Canvas canvas) {
public void draw(@NonNull Canvas canvas) {
if (pos.getResult() == null)
return;
......@@ -62,23 +63,25 @@ public class BoardDrawable extends Drawable {
path.setStrokeCap(Paint.Cap.ROUND);
path.setColor(0xFF009900);
for (int x = 0; x < 10; x++) {
for (int y = 0; y < 13; y++) {
Tile t = pos.getResult().getTiles()[x][y];
if (t.isSet(Tile.SUPER_MINE))
char[] tileLetters = pos.getTileLetters();
int[] tileStates = pos.getResult();
for (int y = 0, index = 0; y < 13; y++) {
for (int x = 0; x < 10; x++, index++) {
int t = tileStates[index];
if ((t & Tile.SUPER_MINE) != 0)
canvas.drawRect(x * 80, y * 80, x * 80 + 80, y * 80 + 80, purpleBg);
else if (t.isSet(Tile.MINE))
else if ((t & Tile.MINE) != 0)
canvas.drawRect(x * 80, y * 80, x * 80 + 80, y * 80 + 80, blackBg);
else if (t.isSet(Tile.PLAYER))
else if ((t & Tile.PLAYER) != 0)
canvas.drawRect(x * 80, y * 80, x * 80 + 80, y * 80 + 80, flipped ? blueBg : orangeBg);
else if (t.isSet(Tile.OPPONENT))
else if ((t & Tile.OPPONENT) != 0)
canvas.drawRect(x * 80, y * 80, x * 80 + 80, y * 80 + 80, flipped ? orangeBg : blueBg);
else
canvas.drawRect(x * 80, y * 80, x * 80 + 80, y * 80 + 80, whiteBg);
canvas.drawText(String.valueOf(t.getLetter()),
x * 80 + 40 - blackText.measureText(String.valueOf(t.getLetter())) / 2,
canvas.drawText(String.valueOf(tileLetters[index]),
x * 80 + 40 - blackText.measureText(String.valueOf(tileLetters[index])) / 2,
y * 80 + 40 - blackText.ascent() / 2,
t.isSet(Tile.MINE | Tile.SUPER_MINE) ? whiteText : blackText);
(t & (Tile.MINE | Tile.SUPER_MINE)) != 0 ? whiteText : blackText);
}
}
if (pos.getCoordinates().length > 2) {
......
package net.pietu1998.wordbasehacker.solver;
import java.util.HashSet;
import java.util.Set;
import android.util.Log;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
public class Board {
private Tile[][] tiles;
private String[] words;
private short[] charMap = new short[65536];
private short charIndex = 0;
private int[] tileStates;
private char[] tileLetters;
private short[] tileMappedLetters;
private TrieNode root;
public Board(Tile[][] tiles, String[] words) {
this.tiles = tiles;
this.words = words;
private List<Possibility> results = new ArrayList<>();
public List<Possibility> getResults() {
return results;
}
public Tile[][] getTiles() {
return tiles;
private class TrieNode {
private TrieNode[] nodes;
private boolean hasChildren = false;
private String content = null;
TrieNode() {
nodes = new TrieNode[charIndex];
}
}
public String[] getWords() {
return words;
public Board(String[] rows, int[] tileStates, String[] words, Game game) {
this.tileStates = tileStates;
tileLetters = new char[130];
tileMappedLetters = new short[130];
for (int y = 0, index = 0; y < 13; y++) {
for (int x = 0; x < 10; x++, index++) {
char letter = tileLetters[index] = rows[y].charAt(x);
int mapping = charMap[letter];
if (mapping == 0)
mapping = charMap[letter] = ++charIndex;
tileMappedLetters[index] = (short) (mapping - 1);
}
}
root = new TrieNode();
for (String word : words) {
if (game.isPlayed(word))
continue;
Board.TrieNode node = root;
for (int i = 0; i < word.length(); i++) {
int mapping = charMap[word.charAt(i)] - 1;
node.hasChildren = true;
Board.TrieNode next = node.nodes[mapping];
if (next == null)
node = node.nodes[mapping] = new Board.TrieNode();
else
node = next;
}
node.content = word;
}
}
public void findWords() {
long start = System.currentTimeMillis();
for (int iteration = 0; iteration < 500; iteration++) {
boolean positions[] = new boolean[130];
byte coords[] = new byte[24];
for (int y = 0, index = 0; y < 13; y++)
for (int x = 0; x < 10; x++, index++)
if ((tileStates[index] & Tile.PLAYER) != 0)
findWordsRecursive(tileMappedLetters, 0, x, y, index, coords, positions, root, results);
if (iteration < 499) results.clear();
}
long end = System.currentTimeMillis();
Log.d("WordbaseHacker", "Finding took " + (end - start) + "ms");
}
public void score(Possibility pos, boolean flipped) {
Tile[][] newTiles = new Tile[10][13];
int oldMines = 0, oldPlayer = 0, oldOpponent = 0, oldDistP = 0, oldDistO = 0;
for (int x = 0; x < 10; x++) {
for (int y = 0; y < 13; y++) {
newTiles[x][y] = tiles[x][y];
if (newTiles[x][y].isSet(Tile.PLAYER)) {
private void findWordsRecursive(short[] tiles, int length, int x, int y, int index, byte[] coords, boolean[] positions, TrieNode node, List<Possibility> results) {
if (length >= 12 || x < 0 || x > 9 || y < 0 || y > 12 || positions[index])
return;
short letter = tiles[index];
node = node.nodes[letter];
if (node == null)
return;
coords[length * 2] = (byte) x;
coords[length * 2 + 1] = (byte) y;
if (node.content != null)
results.add(new Possibility(Arrays.copyOf(coords, length * 2 + 2), node.content));
if (!node.hasChildren)
return;
positions[index] = true;
findWordsRecursive(tiles, length + 1, x, y + 1, index + 10, coords, positions, node, results);
findWordsRecursive(tiles, length + 1, x - 1, y + 1, index + 9, coords, positions, node, results);
findWordsRecursive(tiles, length + 1, x + 1, y + 1, index + 11, coords, positions, node, results);
findWordsRecursive(tiles, length + 1, x - 1, y, index - 1, coords, positions, node, results);
findWordsRecursive(tiles, length + 1, x + 1, y, index + 1, coords, positions, node, results);
findWordsRecursive(tiles, length + 1, x, y - 1, index - 10, coords, positions, node, results);
findWordsRecursive(tiles, length + 1, x - 1, y - 1, index - 11, coords, positions, node, results);
findWordsRecursive(tiles, length + 1, x + 1, y - 1, index - 9, coords, positions, node, results);
positions[index] = false;
}
public void scoreWords(boolean flipped) {
long start = System.currentTimeMillis();
for (int iteration = 0; iteration < 100; iteration++)
for (Possibility pos : results) {
scoreWord(pos, flipped);
pos.setTileLetters(tileLetters);
}
long end = System.currentTimeMillis();
Log.d("WordbaseHacker", "Scoring took " + (end - start) + "ms");
}
private void scoreWord(Possibility pos, boolean flipped) {
int[] newStates = new int[130];
int oldMines = 0, oldPlayer = 0, oldOpponent = 0, oldDistPlayer = 0, oldDistOpponent = 0;
for (int y = 0, index = 0; y < 13; y++) {
for (int x = 0; x < 10; x++, index++) {
int state = newStates[index] = tileStates[index];
if ((state & Tile.PLAYER) != 0) {
oldPlayer++;
oldDistP = max(oldDistP, flipped ? 12 - y : y);
} else if (newTiles[x][y].isSet(Tile.OPPONENT)) {
oldDistPlayer = Math.max(oldDistPlayer, flipped ? 12 - y : y);
} else if ((state & Tile.OPPONENT) != 0) {
oldOpponent++;
oldDistO = max(oldDistO, flipped ? y : 12 - y);
} else if (newTiles[x][y].isSet(Tile.MINE | Tile.SUPER_MINE)) {
oldDistOpponent = Math.max(oldDistOpponent, flipped ? y : 12 - y);
} else if ((state & (Tile.MINE | Tile.SUPER_MINE)) != 0) {
oldMines++;
}
}
}
byte[] coords = pos.getCoordinates();
for (int i = 0; i < coords.length; i += 2)
takeTile(newTiles, coords[i], coords[i + 1]);
Set<Coordinate> connected = new HashSet<Coordinate>();
for (int x = 0; x < 10; x++) {
addConnected(newTiles, x, flipped ? 0 : 12, connected, flipped);
}
for (int x = 0; x < 10; x++) {
for (int y = 0; y < 13; y++) {
if (newTiles[x][y].isSet(Tile.OPPONENT) && !connected.contains(new Coordinate(x, y)))
newTiles[x][y] = newTiles[x][y].modify(0, Tile.OPPONENT);
}
for (int i = 0; i < coords.length; i += 2) {
byte x = coords[i];
byte y = coords[i + 1];
takeTile(newStates, x, y, x + 10 * y);
}
int newMines = 0, newPlayer = 0, newOpponent = 0, newDistP = 0, newDistO = 0;
for (int x = 0; x < 10; x++) {
for (int y = 0; y < 13; y++) {
if (newTiles[x][y].isSet(Tile.PLAYER)) {
boolean[] connected = new boolean[130];
int baseY = flipped ? 0 : 12;
int offset = 10 * baseY;
for (int x = 0; x < 10; x++)
addConnected(newStates, x, baseY, x + offset, connected);
for (int index = 0; index < 130; index++)
if (!connected[index])
newStates[index] &= ~Tile.OPPONENT;
int newMines = 0, newPlayer = 0, newOpponent = 0, newDistPlayer = 0, newDistOpponent = 0;
for (int y = 0, index = 0; y < 13; y++) {
for (int x = 0; x < 10; x++, index++) {
int state = newStates[index];
if ((state & Tile.PLAYER) != 0) {
newPlayer++;
newDistP = max(newDistP, flipped ? 12 - y : y);
} else if (newTiles[x][y].isSet(Tile.OPPONENT)) {
newDistPlayer = Math.max(newDistPlayer, flipped ? 12 - y : y);
} else if ((state & Tile.OPPONENT) != 0) {
newOpponent++;
newDistO = max(newDistO, flipped ? y : 12 - y);
} else if (newTiles[x][y].isSet(Tile.MINE | Tile.SUPER_MINE)) {
newDistOpponent = Math.max(newDistOpponent, flipped ? y : 12 - y);
} else if ((state & (Tile.MINE | Tile.SUPER_MINE)) != 0) {
newMines++;
}
}
}
pos.setScore(new Score(pos.getCoordinates().length / 2, oldMines - newMines, newPlayer - oldPlayer, oldOpponent
- newOpponent, newDistP - oldDistP, oldDistO - newDistO, newDistP == 12));
pos.setResult(new Board(newTiles, words));
}
private static int max(int a, int b) {
return a > b ? a : b;
pos.setScore(new Score(pos.getCoordinates().length / 2, oldMines - newMines,
newPlayer - oldPlayer, oldOpponent - newOpponent,
newDistPlayer - oldDistPlayer, oldDistOpponent - newDistOpponent,
newDistPlayer == 12));
pos.setResult(newStates);
}
private void addConnected(Tile[][] tiles, int x, int y, Set<Coordinate> positions, boolean flipped) {
if (x < 0 || x > 9 || y < 0 || y > 12 || positions.contains(new Coordinate(x, y))
|| !tiles[x][y].isSet(Tile.OPPONENT))
private void takeTile(int[] tiles, int x, int y, int index) {
if (x < 0 || x > 9 || y < 0 || y > 12)
return;
positions.add(new Coordinate(x, y));
addConnected(tiles, x, y + 1, positions, flipped);
addConnected(tiles, x - 1, y + 1, positions, flipped);
addConnected(tiles, x + 1, y + 1, positions, flipped);
addConnected(tiles, x - 1, y, positions, flipped);
addConnected(tiles, x + 1, y, positions, flipped);
addConnected(tiles, x, y - 1, positions, flipped);
addConnected(tiles, x - 1, y - 1, positions, flipped);
addConnected(tiles, x + 1, y - 1, positions, flipped);
int state = tiles[index];
if (state != (state &= ~Tile.SUPER_MINE)) {
takeTile(tiles, x - 1, y - 1, index - 11);
takeTile(tiles, x + 1, y - 1, index - 9);
takeTile(tiles, x - 1, y + 1, index + 9);
takeTile(tiles, x + 1, y + 1, index + 11);
takeTile(tiles, x, y - 1, index - 10);
takeTile(tiles, x - 1, y, index - 1);
takeTile(tiles, x + 1, y, index + 1);
takeTile(tiles, x, y + 1, index + 10);
} else if (state != (state &= ~Tile.MINE)) {
takeTile(tiles, x, y - 1, index - 10);
takeTile(tiles, x - 1, y, index - 1);
takeTile(tiles, x + 1, y, index + 1);
takeTile(tiles, x, y + 1, index + 10);
}
tiles[index] = (state & ~Tile.OPPONENT) | Tile.PLAYER;
}
private static void takeTile(Tile[][] tiles, int x, int y) {
if (x < 0 || x > 9 || y < 0 || y > 12)
private void addConnected(int[] tiles, int x, int y, int index, boolean[] positions) {
if (x < 0 || x > 9 || y < 0 || y > 12 || positions[index] || (tiles[index] & Tile.OPPONENT) == 0)
return;
if (tiles[x][y].isSet(Tile.MINE | Tile.SUPER_MINE)) {
if (tiles[x][y].isSet(Tile.SUPER_MINE)) {
tiles[x][y] = tiles[x][y].modify(0, Tile.SUPER_MINE);
takeTile(tiles, x - 1, y - 1);
takeTile(tiles, x - 1, y + 1);
takeTile(tiles, x + 1, y - 1);
takeTile(tiles, x + 1, y + 1);
}
tiles[x][y] = tiles[x][y].modify(0, Tile.MINE);
takeTile(tiles, x, y - 1);
takeTile(tiles, x - 1, y);
takeTile(tiles, x + 1, y);
takeTile(tiles, x, y + 1);
}
tiles[x][y] = tiles[x][y].modify(Tile.PLAYER, Tile.OPPONENT);
positions[index] = true;
addConnected(tiles, x - 1, y - 1, index - 11, positions);
addConnected(tiles, x, y - 1, index - 10, positions);
addConnected(tiles, x + 1, y - 1, index - 9, positions);
addConnected(tiles, x - 1, y, index - 1, positions);
addConnected(tiles, x + 1, y, index + 1, positions);
addConnected(tiles, x - 1, y + 1, index + 9, positions);
addConnected(tiles, x, y + 1, index + 10, positions);
addConnected(tiles, x + 1, y + 1, index + 11, positions);
}
}
......@@ -10,7 +10,6 @@ public class Game implements Parcelable {
private int id, boardId, opponentId;
private String layout, opponent = null;
private Board board = null;
private Set<String> played;
private boolean flipped;
......@@ -31,14 +30,6 @@ public class Game implements Parcelable {
this.opponent = opponent;
}
public Board getBoard() {
return board;
}
public void setBoard(Board board) {
this.board = board;
}
public int getId() {
return id;
}
......
......@@ -9,7 +9,8 @@ public class Possibility {
@NonNull
private String word;
private Score score;
private Board result;
private int[] result;
private char[] tileLetters;
public Possibility(@NonNull byte[] coordinates, @NonNull String word) {
this.coordinates = coordinates;
......@@ -24,15 +25,22 @@ public class Possibility {
this.score = score;
}
@NonNull
public Board getResult() {
public int[] getResult() {
return result;
}
public void setResult(Board result) {
public void setResult(int[] result) {
this.result = result;
}
public char[] getTileLetters() {
return tileLetters;
}
public void setTileLetters(char[] tileLetters) {
this.tileLetters = tileLetters;
}
@NonNull
public byte[] getCoordinates() {
return coordinates;
......
package net.pietu1998.wordbasehacker.solver;
public class Tile {
public final class Tile {
public static final int PLAYER = 1;
public static final int OPPONENT = 2;
......@@ -8,27 +8,6 @@ public class Tile {
public static final int BASE = 8;
public static final int SUPER_MINE = 16;
private int flags;
private char letter;