wordguess/server.py

254 lines
7.7 KiB
Python
Raw Normal View History

2024-01-05 22:30:58 -05:00
"""
2024-01-05 23:42:44 -05:00
WordGuess Pubnix Game Server
2024-01-05 22:30:58 -05:00
Author: Brandon Rozek
"""
2024-01-05 23:42:44 -05:00
from collections import defaultdict
2024-01-05 22:30:58 -05:00
from datetime import datetime
from functools import lru_cache
2024-01-05 23:42:44 -05:00
from pathlib import Path
2024-01-05 22:30:58 -05:00
from typing import List
2024-01-05 23:42:44 -05:00
import argparse
2024-01-05 22:30:58 -05:00
import os
import pickle
2024-01-05 23:42:44 -05:00
import random
2024-01-05 22:30:58 -05:00
import sqlite3
2024-01-05 23:42:44 -05:00
from wordguess import WordGuess
from pubnix import (
run_simple_server,
receive_message,
send_message
)
2024-01-05 22:30:58 -05:00
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):
2024-01-05 23:42:44 -05:00
"""
Discourage cheating
by making some files unreadable
and some unwritable.
"""
2024-01-05 22:30:58 -05:00
# 33152 = '-rw-------.'
# 33188 = '-rw-r--r--.'
2024-01-06 13:58:48 -05:00
SERVER_FOLDER = Path(__file__).parent.absolute()
2024-01-05 22:30:58 -05:00
os.chmod(__file__, 33152)
2024-01-06 13:58:48 -05:00
os.chmod(f"{SERVER_FOLDER}/pubnix.py", 33188)
os.chmod(f"{SERVER_FOLDER}/wordguess.py", 33188)
os.chmod(f"{SERVER_FOLDER}/words.txt", 33188)
os.chmod(f"{SERVER_FOLDER}/client.py", 33188)
2024-01-05 22:30:58 -05:00
Path(WordGuess.RESULTS_LOCATION).touch(33188)
Path(SAVE_LOCATION).touch(33152)
2024-01-05 23:42:44 -05:00
def game(self, connection, user):
"""
Start a game of Word Guess for
a given user with a specific connection.
"""
# As long as the connection is alive,
# treat the time the same
# NOTE: Timeout specified in pubnix.py
today = datetime.today().date()
wotd = self.get_wotd(today)
# Load from internal state, whether the user
# has already won or made guesses
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])))
# No need to play if the user already won today.
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 the user made an invalid guess, don't
# provide a hint or count it against them.
if not self.valid_guess(message.word):
send_message(
connection,
WordGuess.GuessResponseMessage(
gr,
False,
False,
"",
list(self.letters_guessed[today][user])
)
)
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]
hint = 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],
hint,
list(self.letters_guessed[today][user])
)
)
if self.is_winner[today][user]:
WordGuessServer.save_record(today, user, gr)
2024-01-05 22:30:58 -05:00
@staticmethod
def save_record(date, username, score):
2024-01-05 23:42:44 -05:00
"""
Save score that user acheived into
results database for leaderboard viewing.
"""
2024-01-05 22:30:58 -05:00
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 = []
2024-01-05 23:42:44 -05:00
with open("words.txt", "r") as file:
2024-01-05 22:30:58 -05:00
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):
2024-01-05 23:42:44 -05:00
"""
Determine if a guess is valid,
as invalid guesses don't
get hints or impact guess counts.
"""
2024-01-05 22:30:58 -05:00
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]:
2024-01-05 23:42:44 -05:00
"""
Provide a hint to the user based on the word
guessed and the expected word of the day.
"""
2024-01-05 22:30:58 -05:00
output = ["_"] * len(expected)
counted_pos = set()
2024-01-05 23:42:44 -05:00
# Check for letters in correct positions
2024-01-05 22:30:58 -05:00
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)
2024-01-05 23:42:44 -05:00
# Mark leters that are correct but are not in the
# specified location
2024-01-05 22:30:58 -05:00
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
2024-01-05 23:42:44 -05:00
@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
2024-01-05 22:30:58 -05:00
2024-01-05 23:42:44 -05:00
def make_zero():
return 0
2024-01-05 22:30:58 -05:00
2024-01-05 23:42:44 -05:00
def make_false():
return False
2024-01-05 22:30:58 -05:00
2024-01-05 23:42:44 -05:00
def make_default_dict_zero():
return defaultdict(make_zero)
2024-01-05 22:30:58 -05:00
2024-01-05 23:42:44 -05:00
def make_default_dict_false():
return defaultdict(make_false)
2024-01-05 22:30:58 -05:00
2024-01-05 23:42:44 -05:00
def make_default_dict_set():
return defaultdict(set)
2024-01-06 13:58:48 -05:00
SERVER_FOLDER = Path(__file__).parent.absolute()
SAVE_LOCATION = f"{SERVER_FOLDER}/state.pickle"
2024-01-05 22:30:58 -05:00
if __name__ == "__main__":
2024-01-05 23:42:44 -05:00
# NOTE: The seed must be kept secret otherwise
# players can cheat!
SEED = random.randint(3, 1000000)
2024-01-05 22:30:58 -05:00
w = WordGuessServer(SEED)
2024-01-05 23:42:44 -05:00
# Load game state if existent
2024-01-05 22:30:58 -05:00
if os.path.exists(SAVE_LOCATION):
with open(SAVE_LOCATION, "rb") as file:
w = pickle.load(file)
print("Successfully loaded game state")
2024-01-06 13:58:48 -05:00
print("Seed: ", w.seed)
2024-01-05 22:30:58 -05:00
# Make sure permissions are correct
# to prevent cheating...
w.fix_permissions()
2024-01-05 23:42:44 -05:00
# Start game server
2024-01-05 22:30:58 -05:00
try:
run_simple_server(WordGuess.ADDRESS, w.game)
finally:
2024-01-05 23:42:44 -05:00
# After finishing, save game state
2024-01-05 22:30:58 -05:00
print("Saving game state... ", end="")
with open(SAVE_LOCATION, "wb") as file:
pickle.dump(w, file)
print("Done.")