-
Notifications
You must be signed in to change notification settings - Fork 2
/
wordle.py
362 lines (291 loc) · 9.83 KB
/
wordle.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
# 449 Back-end Project-01 10/22/2022
# Team members:
# Vu Diep
from itertools import cycle
import dataclasses
import sqlite3
import textwrap
import databases
import toml
from quart import Quart, g, abort, request
from quart_schema import QuartSchema, RequestSchemaValidationError, validate_request
from utils.queries import *
from utils.functions import check_pos_valid_letter, add_to_leaderboard
import uuid
from redis import Redis
from rq import Queue
from rq import Retry, Queue
from rq.registry import FailedJobRegistry
app = Quart(__name__)
QuartSchema(app)
app.config.from_file(f"./etc/{__name__}.toml", toml.load)
# Schema classes for data receive from client
@dataclasses.dataclass
class User:
username: str
password: str
@dataclasses.dataclass
class GuessWord:
username: str
game_id: str
guess_word: str
@dataclasses.dataclass
class Game:
id: str
username: str
correct_word: str
win: bool
num_of_guesses: int
@dataclasses.dataclass
class Username:
username: str
@dataclasses.dataclass
class Webhooks:
url: str
# Global Variables
iterator = cycle([0, 1, 2])
q = Queue(connection=Redis())
registry = FailedJobRegistry(queue=q)
# DATABASE CONNECTION
async def _connect_db(num):
if num == 1:
database = databases.Database(app.config["DATABASES"]["URL1"])
await database.connect()
return database
elif num == 2:
database = databases.Database(app.config["DATABASES"]["URL2"])
await database.connect()
return database
else:
database = databases.Database(app.config["DATABASES"]["URL"])
await database.connect()
return database
def _get_db(num):
if not hasattr(g, "sqlite_db"):
g.sqlite_db = _connect_db(num)
return g.sqlite_db
@app.teardown_appcontext
async def close_connection(exception):
db = getattr(g, "_sqlite_db", None)
if db is not None:
await db.disconnect()
# Handle bad routes/errors
@app.errorhandler(404)
def not_found(e):
return {"error": "404 The resource could not be found"}, 404
@app.errorhandler(RequestSchemaValidationError)
def bad_request(e):
return {"error": str(e.validation_error)}, 400
@app.errorhandler(409)
def conflict(e):
return {"error": str(e)}, 409
@app.errorhandler(401)
def unauthorize(e):
return str(e), 401
# API code here for Python people
# ***************************** TEST ROUTES **********************************
@app.route("/game/", methods=["GET"])
def wordle():
"""Game Route (dev only)"""
return textwrap.dedent( """<h1>Welcome to wordle api project Game service</h1>
<p>Vu Diep</p>
""")
# Get a game by id show the correct word for testing
@app.route("/game/<string:id>", methods=["GET"])
async def get_game(id):
"""Get the correct word for a game by game_id (dev only)"""
db = await _get_db(0)
game = await db.fetch_one(
"SELECT * FROM game WHERE id = :id",
values={"id": id}
)
app.logger.info('SELECT * FROM game WHERE id = :id')
if game:
return dict(game)
else:
abort(404)
# *********************************************************************************
@app.route("/game/webhook", methods=["POST"])
@validate_request(Webhooks)
async def register_webhook(data):
db = await _get_db(0)
input_data = dataclasses.asdict(data)
print("here")
callback_url = input_data["url"]
try:
await db.execute("INSERT INTO webhook (url) VALUES (:url)", values={"url": callback_url})
except sqlite3.IntegrityError as e:
abort(409, e)
return {"url" : callback_url}, 200
# Get all games from users
# <int:id> -> user id
# return -> Array [{
# id: int
# num_of_guesses: int
# username: str
# win: bool
# }]
@app.route("/game/user/<string:username>", methods=["GET"])
async def get_all_games_user(username):
"""Get all games by a username, ( win, lose and in progress )
{username} = username
"""
num = next(iterator)
print(num)
db = await _get_db(num)
user_game_active = await db.fetch_all(
"""SELECT id, num_of_guesses, username, win from game
WHERE username=:username""",
values={"username": username}
)
app.logger.info("SELECT id, num_of_guesses, username, win from game WHERE username=:username")
if user_game_active:
return list(map(dict, user_game_active))
else:
abort(404)
# Get all games in progress from users,
# <int:id> -> user id
# return -> Array [{
# id: int
# num_of_guesses: int
# username: str
# win: bool
# }]
@app.route("/game/user/gamesinprogress/<string:username>", methods=["GET"])
async def get_all_games_in_progress_user(username):
"""Get all games that are in progress from a username, won/lost games will not display
"""
num = next(iterator)
db = await _get_db(num)
user_game_active = await db.fetch_all(
"""SELECT id, num_of_guesses, username, win from game
WHERE username=:username AND win != true AND num_of_guesses < 6""",
values={"username": username}
)
app.logger.info("""SELECT id, num_of_guesses, username, win from game
WHERE username=:username AND win != true AND num_of_guesses < 6""")
if user_game_active:
return list(map(dict, user_game_active))
else:
abort(404)
# Get a specific game in progress from user id
# username: -> str
# game_id: -> int, user's id
@app.route("/game/<string:username>/<string:game_id>")
async def get_user_game_in_progress(username, game_id):
"""Get a game in progress"""
num = next(iterator)
db = await _get_db(num)
guess_word_list = await get_guesswords_in_game(
game_id=game_id,
username=username,
db=db,
app=app
)
game_data = await get_game_by_id(
game_id=game_id,
username=username,
db=db,
app=app
)
if not game_data:
abort(404)
game_data["currentGuessWords"] = guess_word_list
return game_data
# Start a Game
# Param: data -> JSON {"username": str}
@app.route("/game/user/start", methods=["POST"])
@validate_request(Username)
async def start_user_new_game(data):
"""Add a new game into database and return the game_id"""
db = await _get_db(0)
user_data = dataclasses.asdict(data)
username = user_data["username"]
game_id = uuid.uuid4()
await add_new_game(game_id=game_id, username=username, db=db)
return {"game_id": game_id, "username": username}
# Add a guess word from user to database
# Param:
# data -> JSON {
# "id": int
# "username": str
# "guess_word": str
# }
@app.route("/game/guess", methods=["POST"])
@validate_request(GuessWord)
async def post_user_guessword(data):
"""Add a guessword into database"""
db = await _get_db(0)
user_guessed = dataclasses.asdict(data) # Data from POST req
game_id = user_guessed["game_id"]
username = user_guessed["username"]
guess_word = user_guessed["guess_word"]
current_game_guesswords_list = await get_guesswords_in_game(username=username, game_id=game_id, db=db, app=app)
num_of_guesses = await get_game_num_guesses(id=game_id, username=username, db=db, app=app)
won = await get_win_query(id=game_id, username=username, db=db, app=app)
# User and game doesn't exist
if not num_of_guesses or not won:
return abort(404)
leaderboard_data={
"game_id" : game_id,
"num_of_guesses": num_of_guesses[0],
"username": username,
"win": won[0]
}
# Game already won or lost
if num_of_guesses[0] >= 6 or won[0]:
return {"numberOfGuesses": num_of_guesses[0], "win": won[0]}
# Check if user already guess the word before
if guess_word in current_game_guesswords_list:
return {"error": "Cannot enter the same word twice"}
# Proceed to check
correct_word = await get_game_correct_word(id=game_id, username=username ,db=db, app=app)
isValid = False
isCorrectWord = False
# Serialize valid data into an array
d = await db.fetch_all("SELECT * FROM valid")
VALID_DATA = [item for t in d for item in t]
try:
if guess_word in VALID_DATA:
await add_user_guessed_word(
id=game_id,
username=username,
guess_word=guess_word,
db=db
)
if guess_word == correct_word:
await set_win_user(id=game_id, username=username, db=db)
# Add to redis queue
leaderboard_data["num_of_guesses"] += 1
leaderboard_data["win"] = True
q.enqueue(add_to_leaderboard, leaderboard_data, retry=Retry(max=3, interval=5))
letter_map = {
'correctPosition' : list(range(6)),
'correctLetterWrongPos': [],
'wrongLetter' : []
}
await increment_guesses(id=game_id, username=username, db=db)
isCorrectWord=True
else:
if num_of_guesses[0] == 5:
leaderboard_data["num_of_guesses"] += 1
leaderboard_data["win"] = False
# Add to redis queue
q.enqueue(add_to_leaderboard, leaderboard_data, retry=Retry(max=3, interval=5))
letter_map = check_pos_valid_letter(
guess_word=guess_word,
correct_word=correct_word
)
await increment_guesses(id=game_id, username=username, db=db)
isValid = True
else:
return {"error": "Invalid word"}
except sqlite3.IntegrityError as e:
abort(409, e)
responseData = {
"guessesRemain": 6 - num_of_guesses[0] - 1,
"isValid": isValid,
"correctWord": isCorrectWord,
"letterPosData": letter_map
}
return responseData, 201 # Return Response