277 lines
9.0 KiB
Python
277 lines
9.0 KiB
Python
import random
|
|
from string import ascii_lowercase
|
|
|
|
from flask import Flask, render_template, session, request, redirect, url_for, flash
|
|
|
|
import sensitive
|
|
from lib import cat_words, ROUNDS, WORD_LENGTH, BLANK
|
|
from play import build_non_letter_hint, build_letter_hint
|
|
from solve import first_round_words, round_words
|
|
|
|
app = Flask(__name__)
|
|
app.config['SECRET_KEY'] = sensitive.SECRET_KEY
|
|
|
|
WORDS = list(cat_words())
|
|
NUM_GAMES = len(WORDS) - 1
|
|
GUESS_SPLITTER = '|'
|
|
|
|
RANDOM_GAME_ID = -1
|
|
|
|
KEY_WORD = 'word'
|
|
KEY_GAME_ID = 'game_id'
|
|
KEY_GUESSES = 'guesses'
|
|
KEY_GUESS = 'guess'
|
|
KEY_STATE = 'state'
|
|
KEY_ROUND = 'round'
|
|
KEY_GUESSES_COLORS = 'guesses_colors'
|
|
|
|
STATE_PLAYING = 'PLAYING'
|
|
STATE_WIN = 'WIN'
|
|
STATE_LOSE = 'LOSE'
|
|
|
|
COLOR_GREY_SHORT = '-'
|
|
COLOR_GREEN_SHORT = 'G'
|
|
COLOR_YELLOW_SHORT = 'Y'
|
|
|
|
COLOR_GREY = 'grey'
|
|
COLOR_GREEN = 'green'
|
|
COLOR_YELLOW = 'yellow'
|
|
|
|
COLOR_MAP = {COLOR_GREY_SHORT: COLOR_GREY,
|
|
COLOR_GREEN_SHORT: COLOR_GREEN,
|
|
COLOR_YELLOW_SHORT: COLOR_YELLOW, }
|
|
COLOR_MAP_REV = {w: k for k, w in COLOR_MAP.items()}
|
|
|
|
ERROR_GAME_ID_RANGE = "game_id must be between 0 and {}".format(NUM_GAMES)
|
|
ERROR_NEED_WORD = 'Need a word'
|
|
ERROR_GUESS_LENGTH = "Need {} letters.".format(WORD_LENGTH)
|
|
ERROR_WORD_NOT_IN_LIST = 'Not in word list: {}'
|
|
ERROR_NO_GAME = 'No game started.'
|
|
|
|
|
|
def verify_word_length(word):
|
|
return len(word) == WORD_LENGTH and all([x in ascii_lowercase for x in word])
|
|
|
|
|
|
def init_game(game_id, word):
|
|
session[KEY_GAME_ID] = game_id
|
|
session[KEY_WORD] = word
|
|
session[KEY_GUESSES] = ''
|
|
session[KEY_STATE] = STATE_PLAYING
|
|
|
|
|
|
def resolve_guesses():
|
|
word = session[KEY_WORD]
|
|
guesses = session.get(KEY_GUESSES, '').split(GUESS_SPLITTER)
|
|
guesses_colored = []
|
|
for guess in guesses:
|
|
non_letters_hints = build_non_letter_hint(guess, word)
|
|
letters_hints = build_letter_hint(guess, word)
|
|
colored_guess = []
|
|
for pos, letter in enumerate(guess):
|
|
if letter in non_letters_hints:
|
|
color = COLOR_GREY
|
|
elif letters_hints[pos] == letter.upper():
|
|
color = COLOR_GREEN
|
|
elif letters_hints[pos] == letter:
|
|
color = COLOR_YELLOW
|
|
else:
|
|
color = COLOR_GREY
|
|
colored_guess.append((letter, color))
|
|
guesses_colored.append(colored_guess)
|
|
return guesses_colored
|
|
|
|
|
|
def build_game_state():
|
|
guesses_colored = resolve_guesses()
|
|
return render_template('game.html',
|
|
guesses=guesses_colored,
|
|
state_playing=session[KEY_STATE] == STATE_PLAYING,
|
|
WORD_LENGTH=WORD_LENGTH,
|
|
KEY_GUESS=KEY_GUESS,
|
|
state=session[KEY_STATE])
|
|
|
|
|
|
# #############################################################################
|
|
# ROUTES
|
|
# #############################################################################
|
|
|
|
|
|
@app.route('/')
|
|
def home():
|
|
num_games = NUM_GAMES
|
|
return render_template('home.html', num_games=num_games, KEY_GAME_ID=KEY_GAME_ID)
|
|
|
|
|
|
@app.route('/find/<word>')
|
|
def find_word(word):
|
|
if not verify_word_length(word):
|
|
flash(ERROR_GUESS_LENGTH)
|
|
return redirect(url_for('home'))
|
|
if word not in WORDS:
|
|
flash(ERROR_WORD_NOT_IN_LIST.format(word))
|
|
return redirect(url_for('home'))
|
|
game_id = WORDS.index(word)
|
|
return redirect(url_for('init_deterministic_game_id', game_id=game_id))
|
|
|
|
|
|
@app.route('/init/random')
|
|
def init_random_game():
|
|
word = random.choice(WORDS)
|
|
init_game(RANDOM_GAME_ID, word)
|
|
return redirect(url_for('random_game'))
|
|
|
|
|
|
@app.route('/init/deterministic', methods=['post'])
|
|
def init_deterministic_game():
|
|
game_id = request.form.get(KEY_GAME_ID)
|
|
return redirect(url_for('init_deterministic_game_id', game_id=game_id))
|
|
|
|
|
|
@app.route('/init/deterministic/<int:game_id>')
|
|
def init_deterministic_game_id(game_id):
|
|
if not 0 <= int(game_id) <= NUM_GAMES:
|
|
flash(ERROR_GAME_ID_RANGE)
|
|
return redirect(url_for('home'))
|
|
word = WORDS[int(game_id)]
|
|
init_game(game_id, word)
|
|
return redirect(url_for('deterministic_game_id', game_id=game_id))
|
|
|
|
|
|
@app.route('/random')
|
|
def random_game():
|
|
if KEY_WORD not in session or KEY_STATE not in session:
|
|
return redirect(url_for('init_random_game'))
|
|
return build_game_state()
|
|
|
|
|
|
@app.route('/deterministic/<int:game_id>')
|
|
def deterministic_game_id(game_id):
|
|
if (KEY_WORD not in session or KEY_STATE not in session or
|
|
(KEY_GAME_ID in session and session[KEY_GAME_ID] != game_id)):
|
|
return redirect(url_for('init_deterministic_game_id', game_id=game_id))
|
|
return build_game_state()
|
|
|
|
|
|
@app.route('/guess', methods=['post'])
|
|
def make_guess():
|
|
if KEY_WORD not in session or KEY_GUESSES not in session:
|
|
flash(ERROR_NEED_WORD)
|
|
return redirect(url_for('home'))
|
|
guess = request.form.get(KEY_GUESS)
|
|
guess = guess.lower()
|
|
if not verify_word_length(guess):
|
|
flash(ERROR_GUESS_LENGTH)
|
|
return build_game_state()
|
|
if guess not in WORDS:
|
|
flash(ERROR_WORD_NOT_IN_LIST.format(guess))
|
|
return build_game_state()
|
|
word = session[KEY_WORD]
|
|
round_i = len(session[KEY_GUESSES].split(GUESS_SPLITTER))
|
|
if round_i >= ROUNDS:
|
|
session[KEY_STATE] = STATE_LOSE
|
|
if guess == word:
|
|
session[KEY_STATE] = STATE_WIN
|
|
add_guess_to_session(guess)
|
|
if KEY_GAME_ID not in session:
|
|
flash(ERROR_NO_GAME)
|
|
return redirect(url_for('home'))
|
|
game_id = session[KEY_GAME_ID]
|
|
if game_id == RANDOM_GAME_ID:
|
|
return redirect(url_for('random_game'))
|
|
return redirect(url_for('deterministic_game_id', game_id=game_id))
|
|
|
|
|
|
def init_solve():
|
|
session[KEY_GUESSES] = ''
|
|
session[KEY_ROUND] = 0
|
|
session[KEY_GUESSES_COLORS] = COLOR_GREY_SHORT * WORD_LENGTH + GUESS_SPLITTER
|
|
|
|
|
|
def add_guess_to_session(guess):
|
|
session[KEY_GUESSES] += '{}{}'.format(guess, GUESS_SPLITTER)
|
|
|
|
|
|
@app.route('/init/solve')
|
|
def init_solve_game():
|
|
init_solve()
|
|
return redirect(url_for('solve_game'))
|
|
|
|
|
|
def build_guess_colors():
|
|
guesses = session[KEY_GUESSES].split(GUESS_SPLITTER)
|
|
guesses_colors = session.get(KEY_GUESSES_COLORS).split(GUESS_SPLITTER)
|
|
all_guesses = []
|
|
for guess, guess_colors in zip(guesses, guesses_colors):
|
|
g = []
|
|
for letter, color_id in zip(list(guess), list(guess_colors)):
|
|
g.append((letter, COLOR_MAP.get(color_id, COLOR_GREY)))
|
|
all_guesses.append(g)
|
|
return all_guesses[:-1]
|
|
|
|
|
|
def resolve_hints():
|
|
all_non_letters, all_positional_letters, all_non_positional_letters = set(), [], []
|
|
all_guesses = build_guess_colors()
|
|
for guess in all_guesses:
|
|
pos_letters, non_pos_letters = '', ''
|
|
for pos, letter_color in enumerate(guess):
|
|
letter, color = letter_color
|
|
if color == COLOR_GREY:
|
|
all_non_letters.add(letter)
|
|
pos_letters += BLANK
|
|
non_pos_letters += BLANK
|
|
elif color == COLOR_YELLOW:
|
|
pos_letters += BLANK
|
|
non_pos_letters += letter
|
|
elif color == COLOR_GREEN:
|
|
pos_letters += letter
|
|
non_pos_letters += BLANK
|
|
all_positional_letters.append(pos_letters)
|
|
all_non_positional_letters.append(non_pos_letters)
|
|
all_non_letters = ''.join(all_non_letters)
|
|
return all_non_letters, all_positional_letters, all_non_positional_letters
|
|
|
|
|
|
@app.route('/solve')
|
|
def solve_game():
|
|
round_i = session[KEY_ROUND]
|
|
len_guesses = len(session[KEY_GUESSES].split(GUESS_SPLITTER)) - 1
|
|
if len_guesses <= round_i:
|
|
if round_i == 0:
|
|
this_round_words = [w for w in first_round_words()]
|
|
else:
|
|
all_non_letters, all_positional_letters, all_non_positional_letters = resolve_hints()
|
|
this_round_words = [w for w in
|
|
round_words(all_non_letters, all_positional_letters, all_non_positional_letters)]
|
|
guess = random.choice(this_round_words)
|
|
add_guess_to_session(guess)
|
|
guesses = build_guess_colors()
|
|
return render_template('solve.html', guesses=guesses, WORD_LENGTH=WORD_LENGTH,
|
|
colors=[COLOR_GREY, COLOR_YELLOW, COLOR_GREEN])
|
|
|
|
|
|
@app.route('/hint', methods=['post'])
|
|
def add_hint():
|
|
letters_colors = [''] * WORD_LENGTH
|
|
for idx in range(WORD_LENGTH):
|
|
letters_colors[idx] = request.form.get("guess-letter-{}".format(idx))
|
|
guess_colors = ''.join([COLOR_MAP_REV.get(letter_color) for letter_color in letters_colors])
|
|
split_colors = session[KEY_GUESSES_COLORS].split(GUESS_SPLITTER)
|
|
if len(split_colors) >= 1:
|
|
# remove the last
|
|
session[KEY_GUESSES_COLORS] = GUESS_SPLITTER.join(split_colors[:-2])
|
|
# TODO: there is some off by one here when adding the last guess_splitter (we need it to display a blank new guess)
|
|
session[KEY_GUESSES_COLORS] += guess_colors + GUESS_SPLITTER + \
|
|
COLOR_GREY_SHORT * WORD_LENGTH + GUESS_SPLITTER
|
|
session[KEY_ROUND] += 1
|
|
return redirect(url_for('solve_game'))
|
|
|
|
|
|
def main():
|
|
app.run(debug=True)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|