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/') 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/') 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/') 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()