mirror of
https://github.com/brandon-rozek/wordguess
synced 2025-10-26 22:31:13 +00:00
Code cleanup
This commit is contained in:
parent
87986d4b2a
commit
8d964f609b
6 changed files with 353 additions and 353 deletions
342
server.py
342
server.py
|
|
@ -1,32 +1,196 @@
|
|||
"""
|
||||
WordGuess Pubnix Game Server
|
||||
Author: Brandon Rozek
|
||||
|
||||
WordGuess game server
|
||||
|
||||
TODO: argparse
|
||||
|
||||
TODO: Multiple users trying to access it at same time?
|
||||
|
||||
TODO: Fix timeout issue
|
||||
|
||||
"""
|
||||
from collections import defaultdict
|
||||
from datetime import datetime
|
||||
from functools import lru_cache
|
||||
from pathlib import Path
|
||||
from typing import List
|
||||
|
||||
import argparse
|
||||
import os
|
||||
import pickle
|
||||
import random
|
||||
import sqlite3
|
||||
|
||||
from wordguess import WordGuess
|
||||
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"
|
||||
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):
|
||||
"""
|
||||
Discourage cheating
|
||||
by making some files unreadable
|
||||
and some unwritable.
|
||||
"""
|
||||
# 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)
|
||||
|
||||
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)
|
||||
|
||||
@staticmethod
|
||||
def save_record(date, username, score):
|
||||
"""
|
||||
Save score that user acheived into
|
||||
results database for leaderboard viewing.
|
||||
"""
|
||||
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 = []
|
||||
with open("words.txt", "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):
|
||||
"""
|
||||
Determine if a guess is valid,
|
||||
as invalid guesses don't
|
||||
get hints or impact guess counts.
|
||||
"""
|
||||
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]:
|
||||
"""
|
||||
Provide a hint to the user based on the word
|
||||
guessed and the expected word of the day.
|
||||
"""
|
||||
output = ["_"] * len(expected)
|
||||
counted_pos = set()
|
||||
|
||||
# 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)
|
||||
|
||||
# Mark leters that are correct but are not in the
|
||||
# specified location
|
||||
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
|
||||
|
||||
|
||||
@lru_cache
|
||||
def char_positions(word: str):
|
||||
|
|
@ -55,142 +219,17 @@ def make_default_dict_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)
|
||||
SAVE_LOCATION = "state.pickle"
|
||||
|
||||
if __name__ == "__main__":
|
||||
# NOTE: The seed must be kept secret otherwise
|
||||
# players can cheat!
|
||||
SEED = random.randint(3, 1000000)
|
||||
print("Seed: ", SEED)
|
||||
|
||||
w = WordGuessServer(SEED)
|
||||
|
||||
# Load data structure if existent
|
||||
# Load game state if existent
|
||||
if os.path.exists(SAVE_LOCATION):
|
||||
with open(SAVE_LOCATION, "rb") as file:
|
||||
w = pickle.load(file)
|
||||
|
|
@ -200,10 +239,11 @@ if __name__ == "__main__":
|
|||
# to prevent cheating...
|
||||
w.fix_permissions()
|
||||
|
||||
# Start game server
|
||||
try:
|
||||
run_simple_server(WordGuess.ADDRESS, w.game)
|
||||
finally:
|
||||
# Save game data structure
|
||||
# After finishing, save game state
|
||||
print("Saving game state... ", end="")
|
||||
with open(SAVE_LOCATION, "wb") as file:
|
||||
pickle.dump(w, file)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue