mirror of
https://github.com/brandon-rozek/wordguess
synced 2024-11-21 20:46:28 -05:00
Initial draft
This commit is contained in:
commit
87986d4b2a
8 changed files with 6457 additions and 0 deletions
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
state.pickle
|
||||||
|
results.db
|
||||||
|
__pycache__
|
83
client.py
Normal file
83
client.py
Normal file
|
@ -0,0 +1,83 @@
|
||||||
|
"""
|
||||||
|
Author: Brandon Rozek
|
||||||
|
|
||||||
|
TODO: Argparse
|
||||||
|
"""
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
from pubnix import (
|
||||||
|
run_simple_client,
|
||||||
|
send_message,
|
||||||
|
receive_message
|
||||||
|
)
|
||||||
|
from wordguess import WordGuess
|
||||||
|
|
||||||
|
WIN_TEXT = lambda score: f"""
|
||||||
|
Congratulations! You solved the word of the day.
|
||||||
|
Come back tomorrow! Your score: {score}
|
||||||
|
"""
|
||||||
|
|
||||||
|
GUESSES_REMAINING = lambda gr: f"You have {gr} guesses remaining."
|
||||||
|
|
||||||
|
LOSE_TEXT = """
|
||||||
|
You ran out of guesses for the day. Come back tomorrow!
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
class WordGuessClient:
|
||||||
|
def __init__(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def start_game(self, client, _):
|
||||||
|
message = receive_message(client, WordGuess.GameStartMessage)
|
||||||
|
guesses_remaining = message.guesses_remaining
|
||||||
|
is_winner = message.is_winner
|
||||||
|
today = datetime.today().date()
|
||||||
|
print(f"""
|
||||||
|
Welcome to WordGuess!
|
||||||
|
|
||||||
|
The goal is to guess the word of the day.
|
||||||
|
This word is {message.num_characters} characters long.
|
||||||
|
|
||||||
|
A * character means a letter was guessed correctly,
|
||||||
|
but in the incorrect position.
|
||||||
|
|
||||||
|
Today is {today}.
|
||||||
|
""")
|
||||||
|
if is_winner:
|
||||||
|
print(WIN_TEXT(guesses_remaining))
|
||||||
|
elif guesses_remaining > 0:
|
||||||
|
print(f"You have {guesses_remaining} guesses remaining")
|
||||||
|
print("\nTo quit, press CTRL-C.")
|
||||||
|
|
||||||
|
try:
|
||||||
|
while not is_winner and guesses_remaining > 0:
|
||||||
|
guess = input("Guess: ")
|
||||||
|
send_message(client, WordGuess.GuessMessage(guess))
|
||||||
|
|
||||||
|
message = receive_message(client, WordGuess.GuessResponseMessage)
|
||||||
|
if not message.valid:
|
||||||
|
print("Not a valid guess, try again.")
|
||||||
|
continue
|
||||||
|
|
||||||
|
guesses_remaining = message.guesses_remaining
|
||||||
|
is_winner = message.winner
|
||||||
|
print(message.hint)
|
||||||
|
print(GUESSES_REMAINING(guesses_remaining))
|
||||||
|
print("Letters Guessed:", sorted(message.letters_guessed))
|
||||||
|
|
||||||
|
if is_winner:
|
||||||
|
print(WIN_TEXT(guesses_remaining))
|
||||||
|
|
||||||
|
if not is_winner:
|
||||||
|
print(LOSE_TEXT)
|
||||||
|
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
w = WordGuessClient()
|
||||||
|
run_simple_client(WordGuess.ADDRESS, w.start_game)
|
97
game.py
Normal file
97
game.py
Normal file
|
@ -0,0 +1,97 @@
|
||||||
|
from collections import defaultdict
|
||||||
|
from datetime import datetime
|
||||||
|
from typing import List
|
||||||
|
|
||||||
|
SEED = 838232
|
||||||
|
|
||||||
|
WORDS = []
|
||||||
|
|
||||||
|
GAME_WORDS = "gamewords.txt"
|
||||||
|
GUESS_WORDS = "guesswords.txt"
|
||||||
|
WORD_LENGTH = 5
|
||||||
|
|
||||||
|
with open("words.txt", "r") as file:
|
||||||
|
lines = file.read().splitlines()
|
||||||
|
WORDS.extend([l for l in lines if len(l) == WORD_LENGTH])
|
||||||
|
|
||||||
|
NUMBER_OF_GUESSES = 6
|
||||||
|
|
||||||
|
TODAY = datetime.today()
|
||||||
|
WORD_OF_THE_DAY = WORDS[(TODAY.year * TODAY.month * TODAY.day * SEED) % len(WORDS) ]
|
||||||
|
|
||||||
|
def valid_guess(guess: str):
|
||||||
|
if len(guess) != len(WORD_OF_THE_DAY):
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Guess needs to be part of our
|
||||||
|
# dictionary
|
||||||
|
if guess not in WORDS:
|
||||||
|
return False
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
def char_positions(word: str):
|
||||||
|
"""
|
||||||
|
Return a dictionary with positions
|
||||||
|
of each character within a word.
|
||||||
|
Ex: "hello" -> {"h": [0], "e": [1], "l": [2, 3], "o": [4]}
|
||||||
|
"""
|
||||||
|
result = defaultdict(list)
|
||||||
|
for i, char in enumerate(word):
|
||||||
|
result[char].append(i)
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
CHAR_POSITIONS = char_positions(WORD_OF_THE_DAY)
|
||||||
|
|
||||||
|
def compare(expected: str, guess: str) -> List[str]:
|
||||||
|
output = ["_"] * len(expected)
|
||||||
|
counted_pos = set()
|
||||||
|
|
||||||
|
|
||||||
|
# (1) Check for letters in correct positions
|
||||||
|
for i, (e_char, g_char) in enumerate(zip(expected, guess)):
|
||||||
|
if e_char == g_char:
|
||||||
|
output[i] = e_char
|
||||||
|
|
||||||
|
|
||||||
|
for i, g_char in enumerate(guess):
|
||||||
|
if g_char in expected and output[i] in ["_", "-"]:
|
||||||
|
for pos in CHAR_POSITIONS[g_char]:
|
||||||
|
if pos not in counted_pos:
|
||||||
|
output[i] = "-"
|
||||||
|
counted_pos.add(pos)
|
||||||
|
break
|
||||||
|
pass
|
||||||
|
|
||||||
|
return output
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
num_guesses = 0
|
||||||
|
print("""
|
||||||
|
Guess words one at a time to guess the game word.
|
||||||
|
|
||||||
|
A - character means a letter was guessed correctly,
|
||||||
|
but in the incorrect position.
|
||||||
|
|
||||||
|
To quit, press CTRL-C.
|
||||||
|
""")
|
||||||
|
# start of the user name interaction
|
||||||
|
print("_ " * WORD_LENGTH)
|
||||||
|
while True:
|
||||||
|
guess = input("Guess: ").lower()
|
||||||
|
if not valid_guess(guess):
|
||||||
|
print("Not a valid guesss")
|
||||||
|
continue
|
||||||
|
num_guesses += 1
|
||||||
|
|
||||||
|
result = compare(WORD_OF_THE_DAY, guess)
|
||||||
|
print(" ".join(result))
|
||||||
|
|
||||||
|
if guess == WORD_OF_THE_DAY:
|
||||||
|
print("You won")
|
||||||
|
break
|
||||||
|
|
||||||
|
if num_guesses >= NUMBER_OF_GUESSES:
|
||||||
|
print("You lost")
|
||||||
|
break
|
24
leaderboard.py
Normal file
24
leaderboard.py
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
"""
|
||||||
|
Author: Brandon Rozek
|
||||||
|
|
||||||
|
View leaderboard information for a particular date
|
||||||
|
|
||||||
|
# TODO: argparse
|
||||||
|
"""
|
||||||
|
|
||||||
|
from wordguess import WordGuess
|
||||||
|
import sqlite3
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
|
||||||
|
DATE = str(datetime.today().date())
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
con = sqlite3.connect(WordGuess.RESULTS_LOCATION)
|
||||||
|
try:
|
||||||
|
cur = con.cursor()
|
||||||
|
res = cur.execute(f"SELECT user, score FROM scores WHERE date = '{DATE}' ORDER BY score DESC")
|
||||||
|
for username, score in res.fetchall():
|
||||||
|
print(username, score)
|
||||||
|
finally:
|
||||||
|
con.close()
|
250
pubnix.py
Normal file
250
pubnix.py
Normal file
|
@ -0,0 +1,250 @@
|
||||||
|
"""
|
||||||
|
Author: Brandon Rozek
|
||||||
|
|
||||||
|
TODO: Handle a user trying to connect multiple
|
||||||
|
times at the same time.
|
||||||
|
|
||||||
|
This might be handled automatically if only one
|
||||||
|
user can play at a time...
|
||||||
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
|
import pwd
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import socket
|
||||||
|
import binascii
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Union
|
||||||
|
from contextlib import contextmanager
|
||||||
|
from dataclasses import dataclass
|
||||||
|
|
||||||
|
# __all__ = ['send_message', 'MESSAGE_BUFFER_LEN', 'ChallengeMessage']
|
||||||
|
|
||||||
|
MESSAGE_BUFFER_LEN = 1024
|
||||||
|
TOKEN_LENGTH = 50
|
||||||
|
TIMEOUT = 5 * 60 # 5 minutes
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@contextmanager
|
||||||
|
def start_client(address):
|
||||||
|
# Create the Unix socket client
|
||||||
|
client = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
||||||
|
|
||||||
|
# Connect to the server
|
||||||
|
try:
|
||||||
|
client.connect(address)
|
||||||
|
except FileNotFoundError:
|
||||||
|
print("Game server is not running at location", address)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
try:
|
||||||
|
yield client
|
||||||
|
finally:
|
||||||
|
client.close()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def run_simple_client(address, fn, force_auth=True):
|
||||||
|
with start_client(address) as client:
|
||||||
|
if force_auth:
|
||||||
|
user = login(client)
|
||||||
|
send_message(client, StartMessage())
|
||||||
|
fn(client, user)
|
||||||
|
|
||||||
|
@contextmanager
|
||||||
|
def client_connection(sock):
|
||||||
|
connection, _ = sock.accept()
|
||||||
|
try:
|
||||||
|
yield connection
|
||||||
|
except (
|
||||||
|
ProtocolException,
|
||||||
|
BrokenPipeError,
|
||||||
|
TimeoutError,
|
||||||
|
ConnectionResetError) as e:
|
||||||
|
# Ignore as client can reconnect
|
||||||
|
pass
|
||||||
|
finally: # clean up the connection
|
||||||
|
connection.close()
|
||||||
|
|
||||||
|
|
||||||
|
def run_simple_server(address, fn, force_auth=True):
|
||||||
|
with start_server(address) as sock:
|
||||||
|
print("Started server at", address)
|
||||||
|
try:
|
||||||
|
while True:
|
||||||
|
with client_connection(sock) as connection:
|
||||||
|
user = None
|
||||||
|
if force_auth:
|
||||||
|
user = authenticate(connection)
|
||||||
|
receive_message(connection, StartMessage)
|
||||||
|
fn(connection, user)
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
print("Stopping server...")
|
||||||
|
|
||||||
|
|
||||||
|
def generate_token(length):
|
||||||
|
# From https://stackoverflow.com/a/41354711
|
||||||
|
return binascii.hexlify(os.urandom(length // 2)).decode()
|
||||||
|
|
||||||
|
def find_owner(path: Union[str, Path]) -> str:
|
||||||
|
return Path(path).owner()
|
||||||
|
|
||||||
|
def generate_challenge(user):
|
||||||
|
return ChallengeMessage(
|
||||||
|
username=user,
|
||||||
|
token=generate_token(TOKEN_LENGTH),
|
||||||
|
location=f"/home/{user}/.pubnix_challenge"
|
||||||
|
)
|
||||||
|
|
||||||
|
def authenticate(connection):
|
||||||
|
# First message should be an authentication message
|
||||||
|
message = receive_message(connection, AuthenticateMessage)
|
||||||
|
user = message.username
|
||||||
|
|
||||||
|
# Send challenge message
|
||||||
|
challenge = generate_challenge(user)
|
||||||
|
send_message(connection, challenge)
|
||||||
|
|
||||||
|
# Second message should be validation message
|
||||||
|
message = receive_message(connection, ValidationMessage)
|
||||||
|
|
||||||
|
# Check that challenge file exists
|
||||||
|
if not os.path.exists(challenge.location):
|
||||||
|
close_with_error(connection, "Challange file doesn't exist")
|
||||||
|
|
||||||
|
# Check if user owns the file
|
||||||
|
if find_owner(challenge.location) != user:
|
||||||
|
close_with_error(connection, "Challange file not owned by user")
|
||||||
|
|
||||||
|
# Make sure we can read the file
|
||||||
|
if not os.access(challenge.location, os.R_OK):
|
||||||
|
close_with_error(connection, "Challange file cannot be read by server")
|
||||||
|
|
||||||
|
# Check contents of challenge file
|
||||||
|
with open(challenge.location, "r") as file:
|
||||||
|
contents = file.read()
|
||||||
|
if contents != challenge.token:
|
||||||
|
close_with_error(connection, "Token within challange file is incorrect")
|
||||||
|
|
||||||
|
# Send authentication successful message
|
||||||
|
send_message(connection, AuthSuccessMessage())
|
||||||
|
return user
|
||||||
|
|
||||||
|
MESSAGE_BUFFER_LEN = 1024
|
||||||
|
|
||||||
|
def login(connection):
|
||||||
|
# Send authentication message
|
||||||
|
user = pwd.getpwuid(os.geteuid()).pw_name
|
||||||
|
message = AuthenticateMessage(username=user)
|
||||||
|
send_message(connection, message)
|
||||||
|
|
||||||
|
# Receive challenge message
|
||||||
|
challenge = receive_message(connection, ChallengeMessage)
|
||||||
|
|
||||||
|
# Write to challenge file
|
||||||
|
with open(challenge.location, "w") as file:
|
||||||
|
file.write(challenge.token)
|
||||||
|
|
||||||
|
# Tell server to check the challenge file
|
||||||
|
send_message(connection, ValidationMessage())
|
||||||
|
|
||||||
|
try:
|
||||||
|
message = receive_message(connection, AuthSuccessMessage)
|
||||||
|
finally:
|
||||||
|
# Delete challenge file
|
||||||
|
os.unlink(challenge.location)
|
||||||
|
|
||||||
|
return user
|
||||||
|
|
||||||
|
|
||||||
|
class MessageEncoder(json.JSONEncoder):
|
||||||
|
def default(self, o):
|
||||||
|
return o.__dict__
|
||||||
|
|
||||||
|
def send_message(connection, message):
|
||||||
|
contents = json.dumps(message, cls=MessageEncoder).encode()
|
||||||
|
connection.sendall(contents)
|
||||||
|
|
||||||
|
def receive_message(connection, cls=None):
|
||||||
|
message = connection.recv(MESSAGE_BUFFER_LEN).decode()
|
||||||
|
try:
|
||||||
|
message = json.loads(message)
|
||||||
|
except Exception:
|
||||||
|
print("Received:", message, flush=True)
|
||||||
|
close_with_error(connection, "Invalid Message Received")
|
||||||
|
|
||||||
|
if cls is not None:
|
||||||
|
try:
|
||||||
|
message = cls(**message)
|
||||||
|
except (TypeError, AssertionError):
|
||||||
|
close_with_error(connection, "Expected message of type")
|
||||||
|
|
||||||
|
return message
|
||||||
|
|
||||||
|
@contextmanager
|
||||||
|
def start_server(address):
|
||||||
|
if os.path.exists(address):
|
||||||
|
print("game.sock exists -- game server already running")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
# Create a unix domain socket
|
||||||
|
sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
||||||
|
sock.settimeout(TIMEOUT)
|
||||||
|
sock.bind(address)
|
||||||
|
sock.listen()
|
||||||
|
|
||||||
|
try:
|
||||||
|
yield sock
|
||||||
|
finally:
|
||||||
|
# Delete game.sock when finished
|
||||||
|
os.unlink(address)
|
||||||
|
|
||||||
|
|
||||||
|
class ProtocolException(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def close_with_error(connection, content: str):
|
||||||
|
message = dict(type="error", message=content)
|
||||||
|
connection.sendall(json.dumps(message).encode())
|
||||||
|
raise ProtocolException()
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class ChallengeMessage:
|
||||||
|
username: str
|
||||||
|
token: str
|
||||||
|
location: str
|
||||||
|
action: str = "challenge"
|
||||||
|
|
||||||
|
def __post_init__(self):
|
||||||
|
assert self.action == "challenge"
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class AuthenticateMessage:
|
||||||
|
username: str
|
||||||
|
action: str = "authenticate"
|
||||||
|
|
||||||
|
def __post_init__(self):
|
||||||
|
assert self.action == "authenticate"
|
||||||
|
assert len(self.username) > 0
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class ValidationMessage:
|
||||||
|
action: str = "validate"
|
||||||
|
def __post_init__(self):
|
||||||
|
assert self.action == "validate"
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class AuthSuccessMessage:
|
||||||
|
type: str = "authentication_success"
|
||||||
|
def __post_init__(self):
|
||||||
|
assert self.type == "authentication_success"
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class StartMessage:
|
||||||
|
action: str = "start"
|
||||||
|
def __post_init__(self):
|
||||||
|
assert self.action == "start"
|
210
server.py
Normal file
210
server.py
Normal file
|
@ -0,0 +1,210 @@
|
||||||
|
"""
|
||||||
|
Author: Brandon Rozek
|
||||||
|
|
||||||
|
WordGuess game server
|
||||||
|
|
||||||
|
TODO: argparse
|
||||||
|
|
||||||
|
TODO: Multiple users trying to access it at same time?
|
||||||
|
|
||||||
|
TODO: Fix timeout issue
|
||||||
|
|
||||||
|
"""
|
||||||
|
from datetime import datetime
|
||||||
|
from functools import lru_cache
|
||||||
|
from pubnix import (
|
||||||
|
run_simple_server,
|
||||||
|
receive_message,
|
||||||
|
send_message
|
||||||
|
)
|
||||||
|
from collections import defaultdict
|
||||||
|
from wordguess import WordGuess
|
||||||
|
from typing import List
|
||||||
|
import os
|
||||||
|
import pickle
|
||||||
|
from pathlib import Path
|
||||||
|
import sqlite3
|
||||||
|
|
||||||
|
SEED = 838211
|
||||||
|
SAVE_LOCATION = "state.pickle"
|
||||||
|
|
||||||
|
@lru_cache
|
||||||
|
def char_positions(word: str):
|
||||||
|
"""
|
||||||
|
Return a dictionary with positions
|
||||||
|
of each character within a word.
|
||||||
|
Ex: "hello" -> {"h": [0], "e": [1], "l": [2, 3], "o": [4]}
|
||||||
|
"""
|
||||||
|
result = defaultdict(list)
|
||||||
|
for i, char in enumerate(word):
|
||||||
|
result[char].append(i)
|
||||||
|
return result
|
||||||
|
|
||||||
|
def make_zero():
|
||||||
|
return 0
|
||||||
|
|
||||||
|
def make_false():
|
||||||
|
return False
|
||||||
|
|
||||||
|
def make_default_dict_zero():
|
||||||
|
return defaultdict(make_zero)
|
||||||
|
|
||||||
|
def make_default_dict_false():
|
||||||
|
return defaultdict(make_false)
|
||||||
|
|
||||||
|
def make_default_dict_set():
|
||||||
|
return defaultdict(set)
|
||||||
|
|
||||||
|
class WordGuessServer:
|
||||||
|
def __init__(self, seed, word_length = 5, guesses_allowed = 6):
|
||||||
|
self.seed = seed
|
||||||
|
self.word_length = word_length
|
||||||
|
self.guesses_allowed = guesses_allowed
|
||||||
|
# date -> user str -> int
|
||||||
|
self.guesses_made = defaultdict(make_default_dict_zero)
|
||||||
|
# date -> user str -> bool
|
||||||
|
self.is_winner = defaultdict(make_default_dict_false)
|
||||||
|
# date -> user str -> set[char]
|
||||||
|
self.letters_guessed = defaultdict(make_default_dict_set)
|
||||||
|
|
||||||
|
def fix_permissions(self):
|
||||||
|
# 33152 = '-rw-------.'
|
||||||
|
# 33188 = '-rw-r--r--.'
|
||||||
|
os.chmod(__file__, 33152)
|
||||||
|
os.chmod("pubnix.py", 33188)
|
||||||
|
os.chmod("wordguess.py", 33188)
|
||||||
|
os.chmod("words.txt", 33188)
|
||||||
|
os.chmod("client.py", 33188)
|
||||||
|
Path(WordGuess.RESULTS_LOCATION).touch(33188)
|
||||||
|
Path(SAVE_LOCATION).touch(33152)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def save_record(date, username, score):
|
||||||
|
con = sqlite3.connect(WordGuess.RESULTS_LOCATION)
|
||||||
|
try:
|
||||||
|
cur = con.cursor()
|
||||||
|
cur.execute(
|
||||||
|
"CREATE TABLE IF NOT EXISTS scores(user TEXT NOT NULL, score INT NOT NULL, date TIMESTAMP NOT NULL, PRIMARY KEY (user, date))"
|
||||||
|
)
|
||||||
|
cur.execute("INSERT INTO scores VALUES (?, ?, ?)", (username, score, date))
|
||||||
|
con.commit()
|
||||||
|
except sqlite3.IntegrityError:
|
||||||
|
print("Cannot write record:", (date, username, score))
|
||||||
|
finally:
|
||||||
|
con.close()
|
||||||
|
|
||||||
|
@lru_cache
|
||||||
|
def get_words(self):
|
||||||
|
words = []
|
||||||
|
game_words = "words.txt"
|
||||||
|
# guess_words = "words.txt"
|
||||||
|
with open(game_words, "r") as file:
|
||||||
|
lines = file.read().splitlines()
|
||||||
|
words.extend([l for l in lines if len(l) == self.word_length])
|
||||||
|
return words
|
||||||
|
|
||||||
|
def get_wotd(self, day):
|
||||||
|
words = self.get_words()
|
||||||
|
index = (day.year * day.month * day.day * self.seed) % len(words)
|
||||||
|
return words[index]
|
||||||
|
|
||||||
|
def valid_guess(self, guess: str):
|
||||||
|
if len(guess) != self.word_length:
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Guess needs to be part of our
|
||||||
|
# dictionary
|
||||||
|
if guess not in self.get_words():
|
||||||
|
return False
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def compare(expected: str, guess: str) -> List[str]:
|
||||||
|
output = ["_"] * len(expected)
|
||||||
|
counted_pos = set()
|
||||||
|
|
||||||
|
# (1) Check for letters in correct positions
|
||||||
|
for i, (e_char, g_char) in enumerate(zip(expected, guess)):
|
||||||
|
if e_char == g_char:
|
||||||
|
output[i] = e_char
|
||||||
|
|
||||||
|
gchar_pos = char_positions(expected)
|
||||||
|
|
||||||
|
for i, g_char in enumerate(guess):
|
||||||
|
if g_char in expected and output[i] in ["_", "*"]:
|
||||||
|
for pos in gchar_pos[g_char]:
|
||||||
|
if pos not in counted_pos:
|
||||||
|
output[i] = "*"
|
||||||
|
counted_pos.add(pos)
|
||||||
|
break
|
||||||
|
|
||||||
|
return output
|
||||||
|
|
||||||
|
def game(self, connection, user):
|
||||||
|
# As long as the connection is alive,
|
||||||
|
# treat the time the same
|
||||||
|
# NOTE: Timeout does exist in pubnix.py
|
||||||
|
today = datetime.today().date()
|
||||||
|
wotd = self.get_wotd(today)
|
||||||
|
|
||||||
|
gr = self.guesses_allowed - self.guesses_made[today][user]
|
||||||
|
is_winner = self.is_winner[today][user]
|
||||||
|
send_message(connection, WordGuess.GameStartMessage(is_winner, self.word_length, gr, list(self.letters_guessed[today][user])))
|
||||||
|
|
||||||
|
if self.is_winner[today][user]:
|
||||||
|
return
|
||||||
|
|
||||||
|
while not self.is_winner[today][user] and self.guesses_made[today][user] < self.guesses_allowed:
|
||||||
|
message = receive_message(connection, WordGuess.GuessMessage)
|
||||||
|
message.word = message.word.lower()
|
||||||
|
|
||||||
|
if not self.valid_guess(message.word):
|
||||||
|
send_message(connection, WordGuess.GuessResponseMessage(gr))
|
||||||
|
continue
|
||||||
|
|
||||||
|
self.is_winner[today][user] = message.word == wotd
|
||||||
|
if not self.is_winner[today][user]:
|
||||||
|
self.guesses_made[today][user] += 1
|
||||||
|
gr = self.guesses_allowed - self.guesses_made[today][user]
|
||||||
|
result = WordGuessServer.compare(wotd, message.word)
|
||||||
|
|
||||||
|
# Populate letters guessed
|
||||||
|
for c in message.word:
|
||||||
|
self.letters_guessed[today][user].add(c)
|
||||||
|
|
||||||
|
send_message(
|
||||||
|
connection,
|
||||||
|
WordGuess.GuessResponseMessage(
|
||||||
|
gr,
|
||||||
|
True,
|
||||||
|
self.is_winner[today][user],
|
||||||
|
result,
|
||||||
|
list(self.letters_guessed[today][user])
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
if self.is_winner[today][user]:
|
||||||
|
WordGuessServer.save_record(today, user, gr)
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
w = WordGuessServer(SEED)
|
||||||
|
|
||||||
|
# Load data structure if existent
|
||||||
|
if os.path.exists(SAVE_LOCATION):
|
||||||
|
with open(SAVE_LOCATION, "rb") as file:
|
||||||
|
w = pickle.load(file)
|
||||||
|
print("Successfully loaded game state")
|
||||||
|
|
||||||
|
# Make sure permissions are correct
|
||||||
|
# to prevent cheating...
|
||||||
|
w.fix_permissions()
|
||||||
|
|
||||||
|
try:
|
||||||
|
run_simple_server(WordGuess.ADDRESS, w.game)
|
||||||
|
finally:
|
||||||
|
# Save game data structure
|
||||||
|
print("Saving game state... ", end="")
|
||||||
|
with open(SAVE_LOCATION, "wb") as file:
|
||||||
|
pickle.dump(w, file)
|
||||||
|
print("Done.")
|
33
wordguess.py
Normal file
33
wordguess.py
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
"""
|
||||||
|
Author: Brandon Rozek
|
||||||
|
|
||||||
|
Contains common data structures
|
||||||
|
between WordGuess client and server
|
||||||
|
"""
|
||||||
|
from dataclasses import dataclass
|
||||||
|
|
||||||
|
class WordGuess:
|
||||||
|
RESULTS_LOCATION = "/home/rozek/repo/wordGuess/results.db"
|
||||||
|
ADDRESS = "/home/rozek/wordGuess/game.sock"
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class GuessMessage:
|
||||||
|
word: str
|
||||||
|
action: str = "guess"
|
||||||
|
def __post_init__(self):
|
||||||
|
assert self.action == "guess"
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class GuessResponseMessage:
|
||||||
|
guesses_remaining: int
|
||||||
|
valid: bool = False
|
||||||
|
winner: bool = False
|
||||||
|
hint: str = ""
|
||||||
|
letters_guessed: str = ""
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class GameStartMessage:
|
||||||
|
is_winner: bool
|
||||||
|
num_characters: int
|
||||||
|
guesses_remaining: int
|
||||||
|
letters_guessed: str
|
Loading…
Reference in a new issue