You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
980 lines
37 KiB
980 lines
37 KiB
from flask import render_template |
|
import sys, cgi, json, datetime, re, time, decimal, subprocess, random, threading, os, pymongo, traceback, base64 |
|
from functools import reduce |
|
|
|
from nav import nav #file in same dir |
|
from BoggleCVPipeline import processImage, BoggleError |
|
|
|
ROOT_DIR = "/srv/boggle" |
|
# GAMES_FILE = ROOT_DIR + "/games.json" |
|
# GAMES_LOCK_FILE = ROOT_DIR + "/games.json.lock" |
|
|
|
IMAGE_UPLOAD_DIR = ROOT_DIR + '/upload-board/' |
|
|
|
DEFINITIONS_FILE = ROOT_DIR + '/lists/CollinsScrabbleWords2019WithDefinitions.json' |
|
WORD_LIST_FILE = ROOT_DIR + '/lists/CollinsScrabbleWords2019.json' |
|
|
|
# remove games that are finished after this many seconds |
|
ARCHIVE_TIMEOUT = 30 #archive a COMPLETED game but keep in the lobby |
|
REMOVE_COMPLETED_FROM_LOBBY_TIMEOUT = 5*60 #remove a COMPLETED game from the lobby |
|
REMOVE_STALE_FROM_LOBBY_TIMEOUT = 2*60*60 #remove a NOT STARTED game from lobby |
|
|
|
EXTRA_SECONDS_TO_SUBMIT = 1 |
|
|
|
#all the dice found in boggle deluxe |
|
BOGGLE_DICE = [ |
|
['O','O','O','T','T','U'], |
|
['D','H','H','N','O','T'], |
|
['N','O','U','T','O','W'], |
|
['A','A','A','S','F','R'], |
|
['A','E','E','M','U','G'], |
|
['A','E','N','N','M','G'], |
|
['A','D','E','N','N','N'], |
|
['D','D','N','R','L','O'], |
|
['C','C','T','S','N','W'], |
|
['F','S','I','P','R','Y'], |
|
['A','E','E','E','E','M'], |
|
['I','R','R','P','H','Y'], |
|
['E','O','T','T','T','M'], |
|
['A','A','F','I','S','R'], |
|
['D','O','L','H','N','R'], |
|
['C','E','S','P','T','I'], |
|
['A','A','E','E','E','E'], |
|
['E','I','I','I','T','T'], |
|
['A','F','I','S','R','Y'], |
|
['C','E','P','I','T','L'], |
|
['E','N','S','S','S','U'], |
|
['C','E','I','I','L','T'], |
|
['D','H','H','O','L','R'], |
|
['G','O','V','R','R','W'], |
|
['K','Z','X','B','J','Qu'] |
|
] |
|
|
|
#the boggle alphabet |
|
ALPHABET = ['A','B','C','D','E','F','G','H','I','J','K','L','M', |
|
'N','O','P','Qu','R','S','T','U','V','W','X','Y','Z'] |
|
|
|
DB_NAME = "boggle" |
|
COLL_NAME = "games" |
|
|
|
with open("/srv/boggle/mongodb-url.txt", 'r') as f: |
|
DB_URL = f.read() |
|
|
|
dbClient = pymongo.MongoClient(DB_URL) |
|
db = dbClient[DB_NAME] |
|
coll = db[COLL_NAME] |
|
|
|
wordList = json.load(open(WORD_LIST_FILE,'r')) |
|
allDefinitions = json.load(open(DEFINITIONS_FILE,'r')) |
|
|
|
|
|
#invitations to play again. the key is the game ID that players are invited from, |
|
# and the value is the new game they are invited to. |
|
invitations = {} |
|
|
|
|
|
#replace quotes |
|
def rq(s): |
|
return s.replace("'", "'").replace('"', '"') |
|
#return cgi.escape(s).replace("'", "'").replace('"', '"') |
|
|
|
def create(size=5, letters=None, minutes=3, board=None): |
|
if letters is None: |
|
if size >=5: |
|
letters = 4 |
|
elif size == 4: |
|
letters = 3 |
|
else: |
|
letters = 2 |
|
|
|
dice = [] #no dice |
|
|
|
#until we have enough dice for this board size |
|
while len(dice) < size*size: |
|
#add a complete set of standard dice |
|
dice.extend(BOGGLE_DICE[:]) |
|
#if the size is 5 or less, only 1 set is required |
|
#if the size is 6 or 7, 2 sets are required, etc. |
|
|
|
random.shuffle(dice) |
|
|
|
wasImported = True #the board was passed in from some other method |
|
if board is None: |
|
wasImported = False #the board was randomly generated rather than given |
|
#construct the board, nested item-by-item |
|
board = [[dice[i*size+j][random.randint(0,5)] for j in range(size)] for i in range(size)] |
|
|
|
#construct a dict with all the info |
|
game = { |
|
"size": size, |
|
"letters": letters, |
|
"minutes": minutes, |
|
"board": board, |
|
"wasImported": wasImported, |
|
"isStarted": False, |
|
"isDone": False, |
|
"isArchived": False, |
|
"players": [], |
|
"timeCreatedSeconds": int(time.time()) |
|
} |
|
return game |
|
|
|
def calculatePoints(word): |
|
length = len(word) |
|
if length > 8: length = 8 |
|
return [0,1,1,1,1,2,3,5,11][length] |
|
|
|
#x, y: the current position on the board |
|
#word: the current word |
|
#used: the spots the word has letters from |
|
# def solve_aux(x, y, word, used, board, words, found, size): |
|
# myused = used[:] |
|
|
|
# myused.append([x,y]); #use the current spot |
|
# myword = word + board[x][y].lower() #add on to the word |
|
|
|
# if myword in words: #if the word is in the list of possible words |
|
# found.append(myword) |
|
# # found[myword] = myused |
|
# words.remove(myword) |
|
# if not any(re.search("^" + myword, word) for word in words): |
|
# return |
|
# for newx in range(x-1,x+2): # +2 because range() is exclusive on the upper bound |
|
# for newy in range(y-1,y+2): |
|
# if (newx>=0 and newy>=0 and newx<size and newy<size and not [newx,newy] in myused): |
|
# solve_aux(newx, newy, myword, myused, board, words, found, size) |
|
|
|
def solve(game): |
|
start = time.time() |
|
board = game["board"] |
|
minWordLength = game["letters"] |
|
size = len(board) |
|
|
|
quantities = [] #the quantities of each letter |
|
|
|
excluded = [] #which letters are not on the board |
|
for letter in ALPHABET: |
|
excluded.append(letter) #copy the alphabet to excluded |
|
quantities.append(0) #fill quantities with 0's |
|
for letter2 in ALPHABET: |
|
excluded.append(letter + letter2) |
|
for x in range(size): |
|
for y in range(size): |
|
letter = board[x][y] |
|
quantities[ALPHABET.index(letter)] += 1 #increase the quantity of the letter |
|
if letter in excluded: #if the letter is in excluded |
|
excluded.remove(letter) #remove the letter from excluded |
|
if letter == 'Qu' and 'U' in excluded: #if the letter is in excluded |
|
excluded.remove('U') #remove the letter from excluded |
|
for newx in range(x-1,x+2): # +2 because range() is exclusive on the upper bound |
|
for newy in range(y-1,y+2): |
|
if (newx>=0 and newy>=0 and newx<size and newy<size |
|
and not (newx == x and newy == y)): |
|
newletter = board[newx][newy] |
|
seq = letter + newletter |
|
if seq in excluded: #if the sequence is in excluded |
|
excluded.remove(seq) #remove the sequence from excluded |
|
if letter == 'Qu': |
|
seq = 'U' + newletter |
|
if seq in excluded: #if the sequence is in excluded |
|
excluded.remove(seq) #remove the sequence from excluded |
|
|
|
#increase the number of U's by the number of Qu's |
|
quantities[ALPHABET.index('U')] += quantities[ALPHABET.index('Qu')] |
|
|
|
# words = [] #the list of possible words |
|
found = [] |
|
|
|
wordList = json.load(open(WORD_LIST_FILE,'r')) |
|
for line in wordList: #sort through each word |
|
if len(line) >= minWordLength: #if the word is long enough |
|
#if the word does not have too many of any letter |
|
if not any(line.count(ALPHABET[i].lower()) > quantities[i] for i in range(len(ALPHABET))): |
|
#if there are no letters not on the board in this word |
|
if not any(letter.lower() in line for letter in excluded): |
|
# words.append(line); #add the word to the list of possible words |
|
if isWordValid(game, line, None): |
|
found.append(line) |
|
|
|
score = 0 |
|
#found = [] |
|
# found = {} |
|
|
|
# for x in range(size): |
|
# for y in range(size): |
|
# used = [] |
|
# word = "" |
|
# solve_aux(x, y, word, used, board, words, found, size) |
|
|
|
score = 0 |
|
for word in found: |
|
score += calculatePoints(word) |
|
|
|
game["words"] = found |
|
# game["paths"] = found |
|
game["maxScore"] = score |
|
game["maxWords"] = len(found) |
|
game["secondsToSolve"] = time.time() - start |
|
return game |
|
|
|
def isWordValidAux(board, word, x, y, used, size): |
|
if len(word) == 0: return True |
|
myused = used[:] |
|
myused.append([x,y]); #use the current spot |
|
l = board[x][y].lower() |
|
if l == "qu": |
|
if len(word) < 2 or word[0:2] != "qu": return False |
|
if word == "qu": return True |
|
newword = word[2:] |
|
else: |
|
if word[0] != l: return False |
|
if word == l: return True |
|
newword = word[1:] |
|
|
|
for newx in range(x-1,x+2): # +2 because range() is exclusive on the upper bound |
|
for newy in range(y-1,y+2): |
|
if (newx>=0 and newy>=0 and newx<size and newy<size and not [newx,newy] in myused): |
|
if isWordValidAux(board, newword, newx, newy, myused, size): |
|
return True |
|
return False |
|
|
|
|
|
def isWordValid(game, word, wordList): |
|
if len(word) < game["letters"] or (wordList is not None and not word in wordList): |
|
return False |
|
board = game["board"] |
|
size = len(board) |
|
if len(word) <= size*size: |
|
for x in range(size): |
|
for y in range(size): |
|
if isWordValidAux(board, word, x, y, [], size): |
|
return True |
|
return False |
|
|
|
|
|
def getGameByID(id): |
|
return coll.find_one({"_id": id}) |
|
|
|
def deleteGameByID(id): |
|
coll.delete_one({"_id": id}) |
|
|
|
def getAllIDs(): |
|
return [x["_id"] for x in coll.find({}, {"_id": 1})] |
|
def newGameID(): |
|
allIDs = getAllIDs() |
|
if len(allIDs) == 0: #if there are no IDs, max([]) will fail |
|
return 0 #so return 1 so it starts with game #0 |
|
else: |
|
return max(getAllIDs())+1 |
|
|
|
def updateAttr(game, attr): |
|
coll.update_one({"_id": game["_id"]}, {"$set": {attr: game[attr]}}) |
|
|
|
|
|
class BackgroundSolver(object): |
|
def __init__(self, game): |
|
self.game = game |
|
thread = threading.Thread(target=self.run, args=()) |
|
thread.daemon = True |
|
thread.start() |
|
|
|
def run(self): |
|
self.game = solve(self.game) |
|
# lockGamesFile() |
|
# games = loadGamesFile() |
|
new_game = getGameByID(self.game["_id"]) |
|
new_game["words"] = self.game["words"] |
|
new_game["maxScore"] = self.game["maxScore"] |
|
new_game["maxWords"] = self.game["maxWords"] |
|
new_game["secondsToSolve"] = self.game["secondsToSolve"] |
|
|
|
if new_game["maxWords"] == 0: |
|
new_game["percentFound"] = 100 |
|
elif not "numWordsPlayersFound" in new_game: |
|
new_game["percentFound"] = 0 |
|
else: |
|
new_game["percentFound"] = new_game["numWordsPlayersFound"] / new_game["maxWords"] * 100 |
|
|
|
coll.update_one({"_id": new_game["_id"]}, {"$set": {"words": new_game["words"], "maxScore": new_game["maxScore"], "maxWords": new_game["maxWords"], "secondsToSolve": new_game["secondsToSolve"], "percentFound": new_game["percentFound"]}}) |
|
|
|
# saveGamesFile(games) |
|
# unlockGamesFile() |
|
|
|
def processAllTypedWords(game): |
|
if not "playerData" in game: |
|
game["playerData"] = {} |
|
if not "typedWords" in game: |
|
game["typedWords"] = {} |
|
for username in game["players"]: |
|
if username in game["typedWords"]: |
|
words = game["typedWords"][username] |
|
else: |
|
words = [] |
|
|
|
if not username in game["players"]: |
|
game["players"].append(username) |
|
|
|
wordsDict = {} |
|
|
|
for word in words: |
|
if not word in wordsDict: |
|
if "words" in game: #if the board is already solved |
|
if word in game["words"]: #check the list of solved words |
|
wordsDict[word] = False #false means it's not a duplicate |
|
else: # if the board has not been solved yet |
|
if isWordValid(game, word, wordList): #manually check using the board |
|
wordsDict[word] = False #false means it's not a duplicate |
|
|
|
for player in game["playerData"]: |
|
playerData = game["playerData"][player] |
|
for word in wordsDict: |
|
if word in playerData["words"]: |
|
wordsDict[word] = True #true means it is a duplicate |
|
if not playerData["words"][word]: |
|
playerData["words"][word] = True |
|
playerData["score"] -= calculatePoints(word) |
|
|
|
score = 0 |
|
for word in wordsDict: |
|
if not wordsDict[word]: |
|
score += calculatePoints(word) |
|
game["playerData"][username] = { |
|
"words": wordsDict, |
|
"score": score, |
|
"numWords": len(wordsDict) |
|
} |
|
|
|
winners = [] |
|
winScore = -1 |
|
playerWords = set([]) |
|
dupes = [] |
|
for player in game["playerData"]: |
|
playerData = game["playerData"][player] |
|
playerWords = playerWords.union(playerData["words"]) |
|
playerScore = playerData["score"] |
|
if playerScore > winScore: |
|
winners = [player] |
|
winScore = playerScore |
|
elif playerScore == winScore and not player in winners: |
|
winners.append(player) |
|
for word in playerData["words"]: |
|
if playerData["words"][word] and not word in dupes: |
|
dupes.append(word) |
|
game["numWordsPlayersFound"] = len(playerWords) |
|
game["duplicates"] = len(dupes) |
|
if ("maxWords" not in game) or game["maxWords"] == 0: |
|
game["percentFound"] = 100 |
|
else: |
|
game["percentFound"] = game["numWordsPlayersFound"] / game["maxWords"] * 100 |
|
game["winners"] = winners |
|
game["winScore"] = winScore |
|
coll.update_one({"_id": game["_id"]}, {"$set": {"playerData": game["playerData"], "typedWords": game["typedWords"], "numWordsPlayersFound": game["numWordsPlayersFound"], "duplicates": game["duplicates"], "percentFound": game["percentFound"], "winners": game["winners"], "winScore": game["winScore"]}}) |
|
return game |
|
|
|
def updateGame(game): |
|
changed = False |
|
|
|
elapsedSinceCreatedSeconds = int(time.time()) - game["timeCreatedSeconds"] |
|
if "timeStartedSeconds" in game: |
|
elapsedSinceStartedSeconds = int(time.time()) - game["timeStartedSeconds"] |
|
else: |
|
elapsedSinceStartedSeconds = 0 |
|
|
|
#check for stale games in the lobby |
|
if not game["isStarted"] and elapsedSinceCreatedSeconds > REMOVE_STALE_FROM_LOBBY_TIMEOUT: |
|
deleteGameByID(game["_id"]) |
|
print("stale game #{} removed from lobby".format(game["_id"])) |
|
return None |
|
|
|
game["secondsLeft"] = game["minutes"]*60 - elapsedSinceStartedSeconds |
|
if game["secondsLeft"] <= -EXTRA_SECONDS_TO_SUBMIT and not game["isDone"]: |
|
game["isDone"] = True |
|
game = processAllTypedWords(game) |
|
changed = True |
|
# if the game has been over for longer than the archive timeout, and it's not already archived, and it's solved |
|
if game["secondsLeft"] <= -ARCHIVE_TIMEOUT and not game["isArchived"] and "words" in game: |
|
game["isArchived"] = True |
|
changed = True |
|
if changed: |
|
coll.update_one({"_id": game["_id"]}, {"$set": {"secondsLeft": game["secondsLeft"], "isDone": game["isDone"], "isArchived": game["isArchived"]}}) |
|
return game |
|
|
|
def toIntOrDefault(v, default): |
|
try: |
|
return int(v) |
|
except ValueError: |
|
return default |
|
|
|
def toFloatOrDefault(v, default): |
|
try: |
|
return float(v) |
|
except ValueError: |
|
return default |
|
|
|
def filterUsername(username): |
|
#only alphanumeric, max length is 32 chars |
|
return re.sub('[^a-zA-Z\d]', '', username)[:32] |
|
|
|
def filterWord(word): |
|
#only letters, convert to lowercase |
|
return re.sub('[^a-z]', '', word.lower()) |
|
|
|
def getAllGames(): |
|
games = coll.find() |
|
games = [updateGame(game) for game in games] |
|
games = [game for game in games if game] #remove entries that are "None" |
|
return games |
|
|
|
def boardFrom5x5Str(board_flat): |
|
board = [] |
|
for i in range(5): |
|
row = [] |
|
for j in range(5): |
|
n = i*5+j |
|
letter = board_flat[n] |
|
if letter == "Q": |
|
letter = "Qu" |
|
row.append(letter) |
|
board.append(row) |
|
return board |
|
|
|
tmp_images = {} |
|
|
|
""" |
|
This method doesn't return html, but JSON data for JS to digest. |
|
It is called with AJAX requests, and the data will be formatted |
|
to update part of the page without reloading the whole thing. |
|
""" |
|
def request_data(form, files): |
|
request = filterWord(form["request"]) |
|
if request == "game": |
|
if "id" in form: |
|
id = toIntOrDefault(form["id"], -1) |
|
# lockGamesFile() |
|
# games = loadGamesFile() |
|
print("game request with id {}".format(id)) |
|
game = getGameByID(id) |
|
if game is None: |
|
print("game not found") |
|
# unlockGamesFile() |
|
return json.dumps({}) |
|
else: |
|
game = updateGame(game) |
|
if game is None: |
|
return json.dumps({}) |
|
print("game found") |
|
# if changed: |
|
# saveGamesFile(games) |
|
# unlockGamesFile() |
|
return json.dumps({"game": game}) |
|
else: |
|
print("game request with no id") |
|
return json.dumps({}) |
|
if request == "games": |
|
# lockGamesFile() |
|
# games = loadGamesFile() |
|
# anyChanged = False |
|
games = getAllGames() |
|
# for id in getAllIDs(): |
|
# updateGame(getGameByID(id)) |
|
# if changed: |
|
# anyChanged = True |
|
# if anyChanged: |
|
# saveGamesFile(games) |
|
# unlockGamesFile() |
|
if "page" in form: |
|
page = filterWord(form["page"]) |
|
if page == "lobby": |
|
games = [game for game in games if game["secondsLeft"] > -REMOVE_COMPLETED_FROM_LOBBY_TIMEOUT] |
|
elif page == "stats": |
|
games = [game for game in games if game["isArchived"]] |
|
return json.dumps({"games": games}) |
|
if request == "basic" and "id" in form: |
|
id = toIntOrDefault(form["id"], -1) |
|
# lockGamesFile() |
|
# games = loadGamesFile() |
|
# unlockGamesFile() |
|
game = getGameByID(id) |
|
if game is not None: |
|
game = updateGame(game) |
|
if game is not None: |
|
return json.dumps({"isStarted": game["isStarted"], "players": game["players"]}) |
|
if request == "savewords" and "id" in form and "words" in form and "username" in form: |
|
id = toIntOrDefault(form["id"], -1) |
|
words = form["words"].split(",") |
|
words = [filterWord(word) for word in words] |
|
username = filterUsername(form["username"]) |
|
# lockGamesFile() |
|
# games = loadGamesFile() |
|
game = getGameByID(id) |
|
if game is not None: |
|
game = updateGame(game) |
|
if not "typedWords" in game: |
|
game["typedWords"] = {} |
|
changed = True |
|
typedWords = game["typedWords"] |
|
if not game["isDone"]: |
|
if not username in typedWords: |
|
typedWords[username] = [] |
|
changed = True |
|
myTypedWords = typedWords[username] |
|
for word in words: |
|
if not word in myTypedWords: |
|
myTypedWords.append(word) |
|
changed = True |
|
if changed: |
|
updateAttr(game, "typedWords") |
|
# coll.update_one({"_id": game["_id"]}, {"$set": {"typedWords": typedWords}) |
|
# saveGamesFile(games) |
|
# unlockGamesFile() |
|
return json.dumps({"typedWords": typedWords[username]}) |
|
if request == "definitions" and "id" in form: |
|
id = toIntOrDefault(form["id"], -1) |
|
game = getGameByID(id) |
|
if game is not None: |
|
definitions = {} |
|
for word in game["words"]: |
|
definitions[word] = allDefinitions[word] |
|
return json.dumps({"definitions": definitions}) |
|
if request == "invitation" and "id" in form: |
|
id = toIntOrDefault(form["id"], -1) |
|
toDelete = [] |
|
for inv in invitations: |
|
newGame = getGameByID(invitations[inv]) |
|
#If the new game is archived, the invitation is useless and can be removed from the list |
|
if newGame is None or newGame["isArchived"]: |
|
toDelete.append(inv) |
|
for inv in toDelete: |
|
del invitations[inv] |
|
print("invitations:", invitations) |
|
if id in invitations: |
|
invitedTo = invitations[id] |
|
return json.dumps({"invitation": invitedTo}) |
|
if request == "upload": |
|
response = {} |
|
# create image directory if not found |
|
if not os.path.isdir(IMAGE_UPLOAD_DIR): |
|
os.mkdir(IMAGE_UPLOAD_DIR) |
|
print(files) |
|
print(len(files)) |
|
if len(files) > 0: |
|
upload = files.getlist("upload")[0] |
|
print("filename: {}".format(upload.filename)) |
|
if upload.filename == "blob": |
|
upload.filename = str(datetime.datetime.now().strftime("%Y%m%d_%H-%M-%S")) |
|
print("new filename: {}".format(upload.filename)) |
|
filename = upload.filename.replace("'","") #filter out ' |
|
destination = IMAGE_UPLOAD_DIR + filename |
|
i=0 |
|
destination_extra = destination |
|
spl = os.path.splitext(destination) |
|
while os.path.exists(destination_extra): |
|
destination_extra = spl[0] + "_" + str(i) + spl[1] |
|
i += 1 |
|
destination = destination_extra |
|
# save file |
|
print("File saved to to:", destination) |
|
upload.save(destination) |
|
message = "File uploaded" |
|
try: |
|
lettersGuessed, confidence = processImage(destination) |
|
response["board"] = boardFrom5x5Str(lettersGuessed) |
|
message += "; " + lettersGuessed |
|
message += "; " + str(confidence) |
|
except Exception as e: |
|
message += "; " + str(e) + "; " + repr(e) + "; " + traceback.format_exc() |
|
with open(destination+".txt", "w") as f: |
|
print("Message saved to to:", destination+".txt") |
|
f.write(message) |
|
else: |
|
message = "no file provided" |
|
response["message"] = message |
|
if os.path.exists(destination+"_warpedimage.png"): |
|
key = str(random.randint(1000000000000000,9999999999999999)) |
|
tmp_images[key] = destination |
|
response["warpedimage"] = "?request=tmpimage&key="+key |
|
return json.dumps(response) |
|
if request == "tmpimage": |
|
if "key" in form and form["key"] in tmp_images: |
|
filename = tmp_images[form["key"]]+"_warpedimage.png" |
|
with open(filename, 'rb') as f: |
|
return f.read() |
|
|
|
return json.dumps({}) |
|
|
|
def do_action(form): |
|
# these are guaranteed by the function that calls this |
|
action = filterWord(form["action"]) |
|
username = filterUsername(form["username"]) |
|
|
|
if action == "create": |
|
board = None |
|
if "preset" in form: |
|
preset = form["preset"] |
|
if preset == "5x5": |
|
size = 5 |
|
letters = 4 |
|
minutes = 3 |
|
elif preset == "4x4": |
|
size = 4 |
|
letters = 3 |
|
minutes = 3 |
|
elif preset == "random": |
|
size = random.randint(2,8) |
|
letters = [0,0, |
|
random.randint(2,3), #2 |
|
random.randint(2,4), #3 |
|
random.randint(2,5), #4 |
|
random.randint(2,6), #5 |
|
random.randint(2,7), #6 |
|
random.randint(2,7), #7 |
|
random.randint(2,7) #8 |
|
] |
|
letters = letters[size] |
|
minutes = random.randint(0,10) |
|
if minutes == 0: minutes = 0.5 |
|
elif preset == "5x5bogglecv" and "board" in form: |
|
board = boardFrom5x5Str(form["board"].split(',')) |
|
print(board) |
|
size = 5 |
|
letters = 4 |
|
minutes = 3 |
|
if "letters" in form: |
|
letters = toIntOrDefault(form["letters"], 4) |
|
if "minutes" in form: |
|
minutes = toFloatOrDefault(form["minutes"], 3) |
|
if "key" in form: |
|
board_file = tmp_images[form["key"]] + "_board_corrected.txt" |
|
with open(board_file, 'w') as f: |
|
f.write(str(board)) |
|
elif preset == "custom" and "size" in form and "letters" in form and "minutes" in form: |
|
size = form["size"] |
|
matched = re.match("^(\d+)x(\d+)$", size) |
|
if matched is None: |
|
size = 5 |
|
else: |
|
size = int(matched.groups()[0]) |
|
if size < 5: |
|
lettersDefault = 3 |
|
else: |
|
lettersDefault = 4 |
|
letters = toIntOrDefault(form["letters"], lettersDefault) |
|
minutes = toFloatOrDefault(form["minutes"], 3) |
|
else: |
|
return "lobby", None |
|
# lockGamesFile() |
|
# games = loadGamesFile() |
|
print("creating game: size={}, letters={}, minutes={}".format(size, letters, minutes)) |
|
game = create(size=size, letters=letters, minutes=minutes, board=board) #usually board is None |
|
game["_id"] = newGameID() |
|
BackgroundSolver(game) |
|
game["players"].append(username) |
|
print(game) |
|
coll.insert_one(game) |
|
# games.append(game) |
|
# saveGamesFile(games) |
|
# unlockGamesFile() |
|
if "inviteFrom" in form: |
|
inviteFrom = toIntOrDefault(form["inviteFrom"], -1) |
|
if inviteFrom >= 0: |
|
invitations[inviteFrom] = game["_id"] |
|
print("invitations:", invitations) |
|
return "pregame", game["_id"] |
|
return "lobby", None |
|
|
|
if action == "join" and "id" in form: |
|
id = toIntOrDefault(form["id"], -1) |
|
# lockGamesFile() |
|
# games = loadGamesFile() |
|
game = getGameByID(id) |
|
if game is None: |
|
# unlockGamesFile() |
|
return "lobby", None |
|
# can't join after the game is finished |
|
if game["isDone"]: |
|
# unlockGamesFile() |
|
return "view", id |
|
elif not username in game["players"]: |
|
game["players"].append(username) |
|
updateAttr(game, "players") |
|
# coll.update_one({"_id": game["_id"]}, {"$set": {"players": game["players"]}) |
|
# saveGamesFile(games) |
|
# unlockGamesFile() |
|
if game["isStarted"]: |
|
return "play", id |
|
else: |
|
return "pregame", id |
|
|
|
if action == "start" and "id" in form: |
|
id = toIntOrDefault(form["id"], -1) |
|
# lockGamesFile() |
|
# games = loadGamesFile() |
|
game = getGameByID(id) |
|
if game is None: |
|
# unlockGamesFile() |
|
return "lobby", None |
|
players = game["players"] |
|
if len(players) > 0 and players[0] == username: |
|
# player is the host, start the game |
|
if not game["isStarted"]: |
|
game["isStarted"] = True |
|
game["timeStartedSeconds"] = int(time.time()) |
|
coll.update_one({"_id": game["_id"]}, {"$set": {"isStarted": game["isStarted"], "timeStartedSeconds": game["timeStartedSeconds"]}}) |
|
# saveGamesFile(games) |
|
# unlockGamesFile() |
|
if not game["isStarted"]: |
|
return "pregame", id |
|
elif not game["isDone"]: |
|
return "play", id |
|
else: |
|
return "view", id |
|
|
|
if action == "cancel" and "id" in form: |
|
id = toIntOrDefault(form["id"], -1) |
|
# lockGamesFile() |
|
# games = loadGamesFile() |
|
game = getGameByID(id) |
|
# works only in the pregame and play phases (i.e. isDone = False) |
|
if game is not None and not game["isDone"]: |
|
# remove this player from the game, promoting the next player to host. |
|
game["players"].remove(username) |
|
# if there are no more players, delete the game |
|
if len(game["players"]) == 0: |
|
games = deleteGameByID(id) |
|
else: |
|
updateAttr(game, "players") |
|
# saveGamesFile(games) |
|
# unlockGamesFile() |
|
return "lobby", None |
|
return "lobby", None |
|
|
|
|
|
def histogram(games, maxGame, prop): |
|
hs = maxGame[prop] |
|
bins = [] |
|
data = [] |
|
if hs < 10: |
|
for i in range(int(hs)+1): |
|
bins.append(i) |
|
data.append(0) |
|
else: |
|
for i in range(1,10+1): #1 to 10 inclusive |
|
bins.append(round(i*hs)/10) |
|
data.append(0) |
|
|
|
for game in games: |
|
s = game[prop] |
|
for j in range(len(bins)): |
|
if round(s*10)/10 <= bins[j]: |
|
data[j] += 1 |
|
break |
|
|
|
return {"labels": bins, "series": [data]} |
|
|
|
def text2bool(text): |
|
return text.lower() == "true" |
|
|
|
def get_param(form, kwargs, name, converter=lambda text: text, fallback=""): |
|
if name in form: |
|
kwargs[name] = converter(form[name]) |
|
else: |
|
kwargs[name] = fallback |
|
|
|
def load_page(form, page=None, id=None): |
|
if page is None: |
|
if "page" in form: |
|
page = filterWord(form["page"]) |
|
else: |
|
page = "lobby" |
|
if not "username" in form and not page in ("stats", "json", "view"): |
|
page = "login" |
|
|
|
if id is None and "id" in form: |
|
id = toIntOrDefault(form["id"], -1) |
|
|
|
if "prev" in form: |
|
prev = filterWord(form["prev"]) |
|
else: |
|
prev = "lobby" |
|
|
|
kwargs = { |
|
"nav": nav, |
|
"active": "boggle", |
|
"stylesheet": "boggle", |
|
"formMethod": "get", |
|
"id": id |
|
} |
|
|
|
|
|
if page == "login": |
|
return render_template("boggle/login.html", page=page, **kwargs) |
|
|
|
get_param(form, kwargs, "username", filterUsername) |
|
|
|
if page == "pregame" and id is not None: |
|
return render_template("boggle/pregame.html", **kwargs) |
|
|
|
if page == "play" and id is not None: |
|
return render_template("boggle/play.html", **kwargs) |
|
|
|
if page == "view" and id is not None: |
|
return render_template("boggle/view.html", prev=prev, **kwargs) |
|
|
|
if page == "stats": |
|
games = getAllGames() |
|
games = [game for game in games if game["isArchived"]] |
|
|
|
#exclude games that were from BoggleCV from the highscore |
|
games = [game for game in games if not ("wasImported" in game and game["wasImported"])] |
|
|
|
get_param(form, kwargs, "selectedUsed", text2bool, False) |
|
|
|
if kwargs["selectedUsed"]: |
|
fallback = "" |
|
else: |
|
fallback = "checked" |
|
|
|
showLabels = [ |
|
"show2x2", "show3x3", "show4x4", "show5x5", "show6x6", "show7x7", "showOtherSizes", |
|
"show2L", "show3L", "show4L", "show5L", "show6L", "show7L", "showOtherL", |
|
"show30Sec", "show1Min", "show2Min", "show3Min", "show4Min", "show5Min", |
|
"show6Min", "show7Min", "show8Min", "show9Min", "show10Min", "showOtherMin" |
|
] |
|
|
|
labelBool = {} |
|
numChecked = 0 |
|
for label in showLabels: |
|
get_param(form, kwargs, label, lambda text: "checked" if text == "on" else "", fallback) |
|
labelBool[label] = kwargs[label] == "checked" |
|
if labelBool[label]: |
|
numChecked += 1 |
|
|
|
kwargs["selectAll"] = "checked" if numChecked >= len(labelBool)/2 else "" |
|
# print(labelBool) |
|
|
|
gamesFiltered = {} |
|
gamesFiltered["5x5"] = [game for game in games if game["size"] == 5 and game["letters"] == 4 and game["minutes"] == 3] |
|
gamesFiltered["4x4"] = [game for game in games if game["size"] == 4 and game["letters"] == 3 and game["minutes"] == 3] |
|
gamesFiltered["Sel"] = [] |
|
for game in games: |
|
if ( |
|
labelBool["show2x2"] and game["size"] == 2 or |
|
labelBool["show3x3"] and game["size"] == 3 or |
|
labelBool["show4x4"] and game["size"] == 4 or |
|
labelBool["show5x5"] and game["size"] == 5 or |
|
labelBool["show6x6"] and game["size"] == 6 or |
|
labelBool["show7x7"] and game["size"] == 7 or |
|
labelBool["showOtherSizes"] and not game["size"] in [2,3,4,5,6,7] |
|
) and ( |
|
labelBool["show2L"] and game["letters"] == 2 or |
|
labelBool["show3L"] and game["letters"] == 3 or |
|
labelBool["show4L"] and game["letters"] == 4 or |
|
labelBool["show5L"] and game["letters"] == 5 or |
|
labelBool["show6L"] and game["letters"] == 6 or |
|
labelBool["show7L"] and game["letters"] == 7 or |
|
labelBool["showOtherL"] and not game["letters"] in [2,3,4,5,6,7] |
|
) and ( |
|
labelBool["show30Sec"] and game["minutes"] == 0.5 or |
|
labelBool["show1Min"] and game["minutes"] == 1 or |
|
labelBool["show2Min"] and game["minutes"] == 2 or |
|
labelBool["show3Min"] and game["minutes"] == 3 or |
|
labelBool["show4Min"] and game["minutes"] == 4 or |
|
labelBool["show5Min"] and game["minutes"] == 5 or |
|
labelBool["show6Min"] and game["minutes"] == 6 or |
|
labelBool["show7Min"] and game["minutes"] == 7 or |
|
labelBool["show8Min"] and game["minutes"] == 8 or |
|
labelBool["show9Min"] and game["minutes"] == 9 or |
|
labelBool["show10Min"] and game["minutes"] == 10 or |
|
labelBool["showOtherMin"] and not game["minutes"] in [0.5,1,2,3,4,5,6,7,8,9,10] |
|
): |
|
gamesFiltered["Sel"].append(game) |
|
gamesFiltered["All"] = games |
|
|
|
for gameType in ["5x5", "4x4", "Sel", "All"]: |
|
kwargs["has"+gameType] = (len(gamesFiltered[gameType]) > 0) |
|
if kwargs["has"+gameType]: |
|
for prop in ["winScore", "numWordsPlayersFound", "percentFound", "maxScore", "maxWords", "duplicates", "secondsToSolve"]: |
|
kwargs[prop+gameType] = reduce(lambda prev, curr: prev if prev[prop] > curr[prop] else curr, gamesFiltered[gameType]) |
|
for prop in ["maxScore", "maxWords"]: |
|
kwargs["low_"+prop+gameType] = reduce(lambda prev, curr: prev if prev[prop] < curr[prop] else curr, gamesFiltered[gameType]) |
|
|
|
get_param(form, kwargs, "sortCol", int, 0) |
|
get_param(form, kwargs, "isAscending", text2bool, False) |
|
|
|
#these correspond to the columns on the stats page |
|
lambdas = [ |
|
lambda game: game["_id"], |
|
lambda game: game["players"][0].lower(), |
|
lambda game: len(game["players"]), |
|
lambda game: game["size"], |
|
lambda game: game["letters"], |
|
lambda game: game["minutes"], |
|
lambda game: game["maxWords"], |
|
lambda game: game["numWordsPlayersFound"], |
|
lambda game: game["percentFound"], |
|
lambda game: game["maxScore"], |
|
lambda game: game["winScore"], |
|
lambda game: game["winners"][0].lower(), |
|
] |
|
|
|
if kwargs["hasSel"]: |
|
for prop in ["winScore", "maxScore", "numWordsPlayersFound", "maxWords", "percentFound", "duplicates", "secondsToSolve"]: |
|
kwargs[prop+"Chart"] = histogram(gamesFiltered["Sel"], kwargs[prop+"Sel"], prop) |
|
|
|
kwargs["sizeChart"] = histogram(gamesFiltered["Sel"], {"size": 7}, "size") |
|
print(kwargs["sizeChart"]) |
|
kwargs["sizeChart"]["labels"] = ["2x2", "3x3", "4x4", "5x5", "6x6", "7x7"] |
|
kwargs["sizeChart"]["series"][0] = kwargs["sizeChart"]["series"][0][2:] #remove 1st 2 items |
|
print(kwargs["sizeChart"]) |
|
|
|
games_sorted = sorted(gamesFiltered["Sel"], key=lambdas[kwargs["sortCol"]], reverse=(not kwargs["isAscending"])) |
|
|
|
# print(kwargs) |
|
return render_template("boggle/stats.html", prev=prev, games=games_sorted, **kwargs) |
|
|
|
if page == "json": |
|
return json.dumps([x for x in coll.find()], indent=2) |
|
|
|
if page == "upload": |
|
return render_template("boggle/upload.html", **kwargs) |
|
|
|
if page == "lobby": |
|
return render_template("boggle/lobby.html", remove_completed_from_lobby_timeout=REMOVE_COMPLETED_FROM_LOBBY_TIMEOUT, **kwargs) |
|
|
|
return "error with page '" + str(page) + "'" |
|
|
|
|
|
""" |
|
This method processes all requests coming in, and passes |
|
them to the correct function. |
|
|
|
There are 3 important fields: |
|
action - an action to be taken before loading the page, |
|
such as leaving a game or submitting a list of words |
|
page - the page to be returned, such as the login, lobby, |
|
pregame, play, and stats pages |
|
request - ask for a certain type of data, such as current |
|
games, past games, data on 1 particular game, or word |
|
definition. This overrides the page, so if a request, |
|
page, and action are all specified, the server will |
|
do the action, then return the requested data, |
|
ignoring the page variable. |
|
""" |
|
def app(request): |
|
# form = request.args | request.form |
|
form = {} |
|
for k, v in request.args.items(): |
|
form[k] = v |
|
for k, v in request.form.items(): |
|
form[k] = v |
|
|
|
print("boggle http request:", form) |
|
page = None |
|
id = None |
|
if "action" in form and "username" in form: |
|
page, id = do_action(form) |
|
|
|
if "request" in form: |
|
return request_data(form, request.files) |
|
else: |
|
return load_page(form, page, id)
|
|
|