Skip to content

Commit

Permalink
split into engine/cEngine modules - still needs some cleanup
Browse files Browse the repository at this point in the history
  • Loading branch information
fogleman committed Feb 25, 2012
1 parent 5c4cfe0 commit f51baee
Show file tree
Hide file tree
Showing 5 changed files with 285 additions and 277 deletions.
31 changes: 14 additions & 17 deletions cEngine.py
@@ -1,24 +1,20 @@
from ctypes import *
import model
from model import Move

DIRECTIONS = {
1: (1, 0),
2: (0, 1),
}
try:
dll = CDLL('_engine.so')
except Exception:
pass

MAX_MOVES = 4096

dll = None

def init(dll_path, dawg_path):
global dll
dll = CDLL(dll_path)
dll.init(dawg_path)
def load_dawg(path):
dll.init(path)

def uninit():
dll.uninit()

class Board(Structure):
class cBoard(Structure):
_fields_ = [
('width', c_int),
('height', c_int),
Expand All @@ -29,7 +25,7 @@ class Board(Structure):
('tiles', POINTER(c_char)),
]

class Move(Structure):
class cMove(Structure):
_fields_ = [
('x', c_int),
('y', c_int),
Expand All @@ -39,7 +35,7 @@ class Move(Structure):
]

def convert_board(board):
b = Board()
b = cBoard()
b.width = board.width
b.height = board.height
b.start = board.start
Expand All @@ -62,16 +58,17 @@ def convert_board(board):
return b

def convert_move(move):
return model.Move(move.x, move.y, DIRECTIONS[move.direction],
move.tiles, move.score, [])
return Move(move.x, move.y, move.direction, move.tiles, move.score, [])

def generate_moves(board, letters):
board = convert_board(board)
letters = ''.join(letters)
moves = (Move * MAX_MOVES)()
moves = (cMove * MAX_MOVES)()
count = dll.generateMoves(byref(board), letters, len(letters), moves, MAX_MOVES)
result = []
for i in xrange(count):
move = convert_move(moves[i])
result.append(move)
return result

load_dawg('files/twl.dawg')
17 changes: 9 additions & 8 deletions engine.c
Expand Up @@ -11,28 +11,28 @@

typedef unsigned int DawgRecord;

DawgRecord* dawg;
DawgRecord *dawg;

char* loadFile(char* path) {
FILE* file = fopen(path, "rb");
char *loadFile(char *path) {
FILE *file = fopen(path, "rb");
fseek(file, 0, SEEK_END);
int length = ftell(file);
rewind(file);
char* buffer = (char*)malloc(length);
char *buffer = (char*)malloc(length);
fread(buffer, 1, length, file);
fclose(file);
return buffer;
}

void init(char* dawgPath) {
dawg = (DawgRecord*)loadFile(dawgPath);
void init(char *path) {
dawg = (DawgRecord *)loadFile(path);
}

void uninit() {
free(dawg);
}

int getDawgRecord(DawgRecord* records, int index, char letter) {
int getDawgRecord(DawgRecord *records, int index, char letter) {
DawgRecord record;
while (1) {
record = records[index];
Expand All @@ -46,7 +46,7 @@ int getDawgRecord(DawgRecord* records, int index, char letter) {
}
}

int checkDawg(DawgRecord* records, char* letters, int length) {
int checkDawg(DawgRecord *records, char *letters, int length) {
int index = 0;
for (int i = 0; i < length; i++) {
index = getDawgRecord(records, index, letters[i]);
Expand All @@ -68,6 +68,7 @@ int checkDawg(DawgRecord* records, char* letters, int length) {
#define EMPTY '.'
#define WILD '?'
#define SKIP '-'

#define INDEX(board, x, y) ((y) * board->width + (x))
#define IS_EMPTY(board, x, y) (board->tiles[INDEX(board, (x), (y))] == EMPTY)
#define IS_LOWER(c) ((c) >= 'a' && (c) <= 'z')
Expand Down
253 changes: 253 additions & 0 deletions engine.py
@@ -0,0 +1,253 @@
from model import (
WILD,
EMPTY,
DIRECTION,
SKIP,
RACK_SIZE,
BINGO,
SENTINEL,
HORIZONTAL,
VERTICAL,
Move,
)

import struct

def load_dawg(path):
with open(path, 'rb') as fp:
data = fp.read()
groups = {}
start = 0
lookup = {}
for index in xrange(len(data) / 4):
block = data[index * 4:index * 4 + 4]
x = struct.unpack('<I', block)[0]
link = x & 0xffffff
letter = chr((x >> 24) & 0x7f)
more = bool((x >> 31) & 1)
lookup[letter] = link
if not more:
groups[start] = lookup
start = index + 1
lookup = {}
for lookup in groups.itervalues():
for letter, link in lookup.iteritems():
lookup[letter] = groups[link] if link else None
return groups[0]

def check_dawg(dawg, word):
word = '%s$' % word.lower()
node = dawg
for letter in word:
if letter in node:
node = node[letter]
else:
return False
return True

def key_letter(tile):
if tile.isupper():
key = WILD
letter = tile.lower()
else:
key = tile
letter = tile
return (key, letter)

def compute_move(board, dawg, x, y, direction, tiles):
mx, my = x, y
dx, dy = DIRECTION[direction]
px, py = int(not dx), int(not dy)
main_word = []
main_score = 0
sub_scores = 0
multiplier = 1
words = []
placed = 0
adjacent = False

# check for dangling tiles before word
ax, ay = x - dx, y - dy
if ax >= 0 and ay >= 0 and board.get_tile(ax, ay) != EMPTY:
return None

for tile in tiles:
# check for board run off
if x < 0 or y < 0 or x >= board.width or y >= board.height:
return None
adjacent = adjacent or board.is_adjacent(x, y)
index = board.index(x, y)
if tile == SKIP:
tile = board.get_tile(x, y)
if tile == EMPTY:
return None
key, letter = key_letter(tile)
main_word.append(letter)
main_score += board.tile_value[key]
else:
placed += 1
key, letter = key_letter(tile)
main_word.append(letter)
main_score += board.tile_value[key] * board.letter_multiplier[index]
multiplier *= board.word_multiplier[index]
# check for perpendicular word
sub_word = [letter]
sub_score = board.tile_value[key] * board.letter_multiplier[index]
n = 1
while True: # prefix
sx = x - px * n
sy = y - py * n
if sx < 0 or sy < 0:
break
tile = board.get_tile(sx, sy)
if tile == EMPTY:
break
key, letter = key_letter(tile)
sub_word.insert(0, letter)
sub_score += board.tile_value[key]
n += 1
n = 1
while True: # suffix
sx = x + px * n
sy = y + py * n
if sx >= board.width or sy >= board.height:
break
tile = board.get_tile(sx, sy)
if tile == EMPTY:
break
key, letter = key_letter(tile)
sub_word.append(letter)
sub_score += board.tile_value[key]
n += 1
if len(sub_word) > 1:
sub_score *= board.word_multiplier[index]
sub_scores += sub_score
sub_word = ''.join(sub_word)
words.append(sub_word)
x += dx
y += dy

# check for dangling tiles after word
if x < board.width and y < board.height and board.get_tile(x, y) != EMPTY:
return None

# check for placed tiles
if placed < 1 or placed > RACK_SIZE or not adjacent:
return None

# compute score
main_score *= multiplier
score = main_score + sub_scores
if placed == RACK_SIZE:
score += BINGO
main_word = ''.join(main_word)
words.insert(0, main_word)

# check words
for word in words:
if not check_dawg(dawg, word):
return None

# build result
return Move(mx, my, direction, tiles, score, words)

def get_horizontal_starts(board, tile_count):
result = [0] * (board.width * board.height)
for y in xrange(board.height):
for x in xrange(board.width):
if x > 0 and not board.is_empty(x - 1, y):
continue
if board.is_empty(x, y):
for i in xrange(tile_count):
if x + i < board.width and board.is_adjacent(x + i, y):
result[board.index(x, y)] = i + 1
break
else:
result[board.index(x, y)] = 1
return result

def get_vertical_starts(board, tile_count):
result = [0] * (board.width * board.height)
for y in xrange(board.height):
for x in xrange(board.width):
if y > 0 and not board.is_empty(x, y - 1):
continue
if board.is_empty(x, y):
for i in xrange(tile_count):
if y + i < board.height and board.is_adjacent(x, y + i):
result[board.index(x, y)] = i + 1
break
else:
result[board.index(x, y)] = 1
return result

def _generate(board, x, y, dx, dy, counts, node, tiles, min_tiles, results):
if len(tiles) >= min_tiles and SENTINEL in node:
results.append(''.join(tiles))
if x >= board.width or y >= board.height:
return
tile = board.get_tile(x, y).lower()
if tile == EMPTY:
for tile in counts:
if counts[tile]:
if tile == WILD:
for letter in xrange(26):
tile = chr(ord('a') + letter)
if tile in node:
counts[WILD] -= 1
tiles.append(tile.upper())
_generate(board, x + dx, y + dy, dx, dy, counts,
node[tile], tiles, min_tiles, results)
tiles.pop()
counts[WILD] += 1
else:
if tile in node:
counts[tile] -= 1
tiles.append(tile)
_generate(board, x + dx, y + dy, dx, dy, counts,
node[tile], tiles, min_tiles, results)
tiles.pop()
counts[tile] += 1
else:
if tile in node:
tiles.append(SKIP)
_generate(board, x + dx, y + dy, dx, dy, counts, node[tile],
tiles, min_tiles, results)
tiles.pop()

def generate(board, dawg, tiles):
moves = []
counts = dict((letter, tiles.count(letter)) for letter in set(tiles))
hstarts = get_horizontal_starts(board, len(tiles))
vstarts = get_vertical_starts(board, len(tiles))
for y in xrange(board.height):
for x in xrange(board.width):
index = board.index(x, y)
min_tiles = hstarts[index]
if min_tiles:
direction = HORIZONTAL
dx, dy = DIRECTION[direction]
results = []
_generate(board, x, y, dx, dy, counts, dawg, [],
min_tiles, results)
for result in results:
move = compute_move(board, dawg, x, y, direction, result)
if move:
moves.append(move)
min_tiles = vstarts[index]
if min_tiles:
direction = VERTICAL
dx, dy = DIRECTION[direction]
results = []
_generate(board, x, y, dx, dy, counts, dawg, [],
min_tiles, results)
for result in results:
move = compute_move(board, dawg, x, y, direction, result)
if move:
moves.append(move)
return moves

def generate_moves(board, letters):
return generate(board, DAWG, letters)

DAWG = load_dawg('files/twl.dawg')

0 comments on commit f51baee

Please sign in to comment.