Store game in session instead of cookie
Browse files- .gitignore +3 -1
- Dockerfile +2 -2
- pyproject.toml +1 -1
- src/wordle/game.py +1 -1
- src/wordle/tw.py +9 -2
- src/wordle/ui.py +31 -19
- uv.lock +1 -1
.gitignore
CHANGED
|
@@ -1,4 +1,6 @@
|
|
| 1 |
.venv
|
| 2 |
app.css
|
| 3 |
.sesskey
|
| 4 |
-
__pycache__
|
|
|
|
|
|
|
|
|
| 1 |
.venv
|
| 2 |
app.css
|
| 3 |
.sesskey
|
| 4 |
+
__pycache__
|
| 5 |
+
.vscode/
|
| 6 |
+
tailwind.config.js
|
Dockerfile
CHANGED
|
@@ -15,6 +15,6 @@ COPY --chown=user src ./src
|
|
| 15 |
RUN touch README.md
|
| 16 |
|
| 17 |
RUN uv sync --frozen
|
| 18 |
-
RUN . .venv/bin/activate
|
| 19 |
|
| 20 |
-
CMD ["uv", "run", "python", "src/wordle/ui.py"]
|
|
|
|
|
|
| 15 |
RUN touch README.md
|
| 16 |
|
| 17 |
RUN uv sync --frozen
|
|
|
|
| 18 |
|
| 19 |
+
# CMD ["uv", "run", "python", "src/wordle/ui.py"]
|
| 20 |
+
CMD ["uv", "run", "uvicorn", "--port", "5001", "--host", "0.0.0.0", "wordle.ui:app"]
|
pyproject.toml
CHANGED
|
@@ -5,7 +5,7 @@ description = "A simple wordle clone with fasthtml"
|
|
| 5 |
readme = "README.md"
|
| 6 |
requires-python = ">=3.10"
|
| 7 |
dependencies = [
|
| 8 |
-
"python-fasthtml>=0.4.
|
| 9 |
]
|
| 10 |
|
| 11 |
# [tool.uv.sources]
|
|
|
|
| 5 |
readme = "README.md"
|
| 6 |
requires-python = ">=3.10"
|
| 7 |
dependencies = [
|
| 8 |
+
"python-fasthtml>=0.4.6",
|
| 9 |
]
|
| 10 |
|
| 11 |
# [tool.uv.sources]
|
src/wordle/game.py
CHANGED
|
@@ -68,7 +68,7 @@ class Game:
|
|
| 68 |
# Return updated squares and keys
|
| 69 |
def keypress(self, key: str):
|
| 70 |
keys = []
|
| 71 |
-
if key == "
|
| 72 |
word = self.current
|
| 73 |
squares = self._enter()
|
| 74 |
keys = word if squares else []
|
|
|
|
| 68 |
# Return updated squares and keys
|
| 69 |
def keypress(self, key: str):
|
| 70 |
keys = []
|
| 71 |
+
if key == "GO":
|
| 72 |
word = self.current
|
| 73 |
squares = self._enter()
|
| 74 |
keys = word if squares else []
|
src/wordle/tw.py
CHANGED
|
@@ -11,6 +11,7 @@ app, rt = fast_app(hdrs=[
|
|
| 11 |
)
|
| 12 |
|
| 13 |
# Add public/app.css to your .gitignore
|
|
|
|
| 14 |
```
|
| 15 |
|
| 16 |
Acknowledgement: This code is heavily inspired by the [pytailwindcss](https://github.com/timonweb/pytailwindcss) project.
|
|
@@ -54,7 +55,13 @@ class Tailwind:
|
|
| 54 |
`public/app.css` should be in .gitignore
|
| 55 |
"""
|
| 56 |
|
| 57 |
-
def __init__(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 58 |
self.dir = tempfile.TemporaryDirectory()
|
| 59 |
path = Path(self.dir.name)
|
| 60 |
if static_path is None:
|
|
@@ -81,7 +88,7 @@ class Tailwind:
|
|
| 81 |
)
|
| 82 |
subprocess.run([str(cli)] + args, **kwargs)
|
| 83 |
return self
|
| 84 |
-
|
| 85 |
def get_link_tag(self):
|
| 86 |
from fasthtml.common import Link
|
| 87 |
|
|
|
|
| 11 |
)
|
| 12 |
|
| 13 |
# Add public/app.css to your .gitignore
|
| 14 |
+
# serve(reload_excludes=["public/app.css"])
|
| 15 |
```
|
| 16 |
|
| 17 |
Acknowledgement: This code is heavily inspired by the [pytailwindcss](https://github.com/timonweb/pytailwindcss) project.
|
|
|
|
| 55 |
`public/app.css` should be in .gitignore
|
| 56 |
"""
|
| 57 |
|
| 58 |
+
def __init__(
|
| 59 |
+
self,
|
| 60 |
+
static_path=None,
|
| 61 |
+
filename="app.css",
|
| 62 |
+
cfg: str = DEFAULT_CONFIG,
|
| 63 |
+
css: str = DEFAULT_SOURCE_CSS,
|
| 64 |
+
):
|
| 65 |
self.dir = tempfile.TemporaryDirectory()
|
| 66 |
path = Path(self.dir.name)
|
| 67 |
if static_path is None:
|
|
|
|
| 88 |
)
|
| 89 |
subprocess.run([str(cli)] + args, **kwargs)
|
| 90 |
return self
|
| 91 |
+
|
| 92 |
def get_link_tag(self):
|
| 93 |
from fasthtml.common import Link
|
| 94 |
|
src/wordle/ui.py
CHANGED
|
@@ -1,4 +1,4 @@
|
|
| 1 |
-
from fasthtml.common import fast_app, Div, serve, Button,
|
| 2 |
from wordle.tw import Tailwind
|
| 3 |
from wordle.game import Eval, Game, State
|
| 4 |
|
|
@@ -6,18 +6,22 @@ from wordle.game import Eval, Game, State
|
|
| 6 |
app, rt = fast_app(hdrs=[Tailwind("./").run().get_link_tag()], pico=False, static_path="./")
|
| 7 |
|
| 8 |
|
| 9 |
-
@
|
| 10 |
-
def
|
| 11 |
-
|
|
|
|
|
|
|
| 12 |
|
| 13 |
|
| 14 |
-
@
|
| 15 |
-
def
|
| 16 |
-
|
|
|
|
|
|
|
| 17 |
|
| 18 |
|
| 19 |
-
def make_app(g:
|
| 20 |
-
return
|
| 21 |
Div(
|
| 22 |
H1(Button("WORDLE", hx_post="/new"), cls="text-3xl font-bold"),
|
| 23 |
cls="w-full flex flex-row place-content-center text-black",
|
|
@@ -41,18 +45,25 @@ def make_app(g: "Game"):
|
|
| 41 |
|
| 42 |
|
| 43 |
@rt("/keypress")
|
| 44 |
-
def put(
|
| 45 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 46 |
squares, keys = g.keypress(key)
|
|
|
|
| 47 |
return (
|
| 48 |
-
cookie("data", g.to_str()),
|
| 49 |
*[make_square(g, i) for i in squares],
|
| 50 |
*[make_key(g, k) for k in keys],
|
| 51 |
make_status(g),
|
| 52 |
)
|
| 53 |
|
| 54 |
|
| 55 |
-
def make_status(g:
|
| 56 |
msgs = {State.WIN: "You win", State.LOSE: "You lose"}
|
| 57 |
return Div(
|
| 58 |
msgs.get(g.state),
|
|
@@ -62,7 +73,7 @@ def make_status(g: "Game"):
|
|
| 62 |
)
|
| 63 |
|
| 64 |
|
| 65 |
-
def make_square(g:
|
| 66 |
cls = "grid h-12 w-12 sm:h-14 sm:w-14 place-items-center rounded-sm text-2xl font-bold"
|
| 67 |
c, state = g.get_square_state(i)
|
| 68 |
styles = {
|
|
@@ -75,10 +86,10 @@ def make_square(g: "Game", i):
|
|
| 75 |
return Div(c, cls=cls + " " + styles[state], id=f"sq{i}", hx_swap_oob="true")
|
| 76 |
|
| 77 |
|
| 78 |
-
def make_key(g:
|
| 79 |
state = g.get_keyboard_state(key)
|
| 80 |
-
cls = "grid h-14 cursor-pointer items-center rounded font-semibold"
|
| 81 |
-
size = "
|
| 82 |
styles = {
|
| 83 |
Eval.CORRECT: GREEN,
|
| 84 |
Eval.EXIST: YELLOW,
|
|
@@ -93,6 +104,7 @@ def make_key(g: "Game", key: str):
|
|
| 93 |
|
| 94 |
|
| 95 |
GREEN, YELLOW, GRAY = "bg-[#20AA57]", "bg-[#E5B22D]", "bg-[#989898]"
|
| 96 |
-
KEYBOARD = [list("QWERTYUIOP"), list("ASDFGHJKL"), ["
|
| 97 |
|
| 98 |
-
|
|
|
|
|
|
| 1 |
+
from fasthtml.common import fast_app, Div, serve, Button, H1
|
| 2 |
from wordle.tw import Tailwind
|
| 3 |
from wordle.game import Eval, Game, State
|
| 4 |
|
|
|
|
| 6 |
app, rt = fast_app(hdrs=[Tailwind("./").run().get_link_tag()], pico=False, static_path="./")
|
| 7 |
|
| 8 |
|
| 9 |
+
@app.get("/")
|
| 10 |
+
def homepage(session):
|
| 11 |
+
if data := session.get("game"):
|
| 12 |
+
return make_app(Game.from_str(data))
|
| 13 |
+
return new_game.__wrapped__(session)
|
| 14 |
|
| 15 |
|
| 16 |
+
@app.post("/new")
|
| 17 |
+
def new_game(session):
|
| 18 |
+
g = Game.from_str(None)
|
| 19 |
+
session["game"] = g.to_str()
|
| 20 |
+
return make_app(g)
|
| 21 |
|
| 22 |
|
| 23 |
+
def make_app(g: Game):
|
| 24 |
+
return Div(
|
| 25 |
Div(
|
| 26 |
H1(Button("WORDLE", hx_post="/new"), cls="text-3xl font-bold"),
|
| 27 |
cls="w-full flex flex-row place-content-center text-black",
|
|
|
|
| 45 |
|
| 46 |
|
| 47 |
@rt("/keypress")
|
| 48 |
+
def put(session, key: str):
|
| 49 |
+
if "game" not in session:
|
| 50 |
+
return Div(
|
| 51 |
+
"The app does not work inside iframe. Try it here https://phihung-wordle.hf.space/",
|
| 52 |
+
cls="h-screen flex items-center justify-center text-xl",
|
| 53 |
+
hx_swap_oob="true",
|
| 54 |
+
id="app",
|
| 55 |
+
)
|
| 56 |
+
g = Game.from_str(session["game"])
|
| 57 |
squares, keys = g.keypress(key)
|
| 58 |
+
session["game"] = g.to_str()
|
| 59 |
return (
|
|
|
|
| 60 |
*[make_square(g, i) for i in squares],
|
| 61 |
*[make_key(g, k) for k in keys],
|
| 62 |
make_status(g),
|
| 63 |
)
|
| 64 |
|
| 65 |
|
| 66 |
+
def make_status(g: Game):
|
| 67 |
msgs = {State.WIN: "You win", State.LOSE: "You lose"}
|
| 68 |
return Div(
|
| 69 |
msgs.get(g.state),
|
|
|
|
| 73 |
)
|
| 74 |
|
| 75 |
|
| 76 |
+
def make_square(g: Game, i):
|
| 77 |
cls = "grid h-12 w-12 sm:h-14 sm:w-14 place-items-center rounded-sm text-2xl font-bold"
|
| 78 |
c, state = g.get_square_state(i)
|
| 79 |
styles = {
|
|
|
|
| 86 |
return Div(c, cls=cls + " " + styles[state], id=f"sq{i}", hx_swap_oob="true")
|
| 87 |
|
| 88 |
|
| 89 |
+
def make_key(g: Game, key: str):
|
| 90 |
state = g.get_keyboard_state(key)
|
| 91 |
+
cls = "grid h-14 cursor-pointer items-center rounded font-semibold touch-manipulation"
|
| 92 |
+
size = " w-14 sm:w-16 " if len(key) > 1 else " w-9 sm:w-10 "
|
| 93 |
styles = {
|
| 94 |
Eval.CORRECT: GREEN,
|
| 95 |
Eval.EXIST: YELLOW,
|
|
|
|
| 104 |
|
| 105 |
|
| 106 |
GREEN, YELLOW, GRAY = "bg-[#20AA57]", "bg-[#E5B22D]", "bg-[#989898]"
|
| 107 |
+
KEYBOARD = [list("QWERTYUIOP"), list("ASDFGHJKL"), ["GO"] + list("ZXCVBNM") + ["DEL"]]
|
| 108 |
|
| 109 |
+
if __name__ == "__main__":
|
| 110 |
+
serve(reload_excludes=["./app.css"])
|
uv.lock
CHANGED
|
@@ -564,7 +564,7 @@ dev = [
|
|
| 564 |
]
|
| 565 |
|
| 566 |
[package.metadata]
|
| 567 |
-
requires-dist = [{ name = "python-fasthtml", specifier = ">=0.4.
|
| 568 |
|
| 569 |
[package.metadata.requires-dev]
|
| 570 |
dev = [{ name = "ruff", specifier = ">=0.6.3" }]
|
|
|
|
| 564 |
]
|
| 565 |
|
| 566 |
[package.metadata]
|
| 567 |
+
requires-dist = [{ name = "python-fasthtml", specifier = ">=0.4.6" }]
|
| 568 |
|
| 569 |
[package.metadata.requires-dev]
|
| 570 |
dev = [{ name = "ruff", specifier = ">=0.6.3" }]
|