Compare commits

...

12 Commits

Author SHA1 Message Date
Hazel Noack
0dc8871bb8 layed out websockets 2025-07-01 12:51:40 +02:00
Hazel Noack
74ea6270c8 removed templates from main 2025-07-01 12:20:58 +02:00
Hazel Noack
a1d2669dad use random word library 2025-07-01 12:17:59 +02:00
amnesia
ad286d745f added frontend that can play very bare bones 2025-06-30 21:22:07 +02:00
Hazel Noack
9c8b8177ad fixed changing of pointers 2025-06-30 14:23:13 +02:00
Hazel Noack
1e0a4c6317 implemented guess endpoint 2025-06-30 13:11:23 +02:00
Hazel Noack
748c2de536 implemented functionality to guess a letter 2025-06-30 12:44:06 +02:00
Hazel Noack
9ea2158def only return lowercase 2025-06-30 12:21:41 +02:00
Hazel Noack
18e9322013 removed non alpha words from dict 2025-06-30 12:20:01 +02:00
Hazel Noack
04f84e34c0 removed non alpha words from dict 2025-06-30 12:19:17 +02:00
Hazel Noack
dec1323c47 removed non alpha words from dict 2025-06-30 12:18:52 +02:00
Hazel Noack
3f2f19943a added getting words 2025-06-30 12:15:58 +02:00
14 changed files with 380 additions and 98 deletions

5
.vscode/settings.json vendored Normal file
View File

@@ -0,0 +1,5 @@
{
"cSpell.words": [
"petname"
]
}

View File

@@ -1,2 +1,3 @@
# hangman # hangman
- [ ] clean up https://gitea.elara.ws/Hazel/go-words as actual library

3
go.mod
View File

@@ -3,7 +3,10 @@ module gitea.elara.ws/Hazel/hangman
go 1.24.2 go 1.24.2
require ( require (
gitea.elara.ws/Hazel/go-words v0.0.0-20250701093631-6125867cea5a // indirect
gitea.elara.ws/Hazel/words v1.0.5 // indirect
github.com/dustinkirkland/golang-petname v0.0.0-20240428194347-eebcea082ee0 // indirect github.com/dustinkirkland/golang-petname v0.0.0-20240428194347-eebcea082ee0 // indirect
github.com/gorilla/websocket v1.5.3 // indirect
github.com/labstack/echo/v4 v4.13.4 // indirect github.com/labstack/echo/v4 v4.13.4 // indirect
github.com/labstack/gommon v0.4.2 // indirect github.com/labstack/gommon v0.4.2 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-colorable v0.1.14 // indirect

8
go.sum
View File

@@ -1,5 +1,13 @@
gitea.elara.ws/Hazel/go-words v0.0.0-20250701093631-6125867cea5a h1:E5BgJBsBKL1MNW1W6HK69XYXvDKtYLfRezl1nh27B5g=
gitea.elara.ws/Hazel/go-words v0.0.0-20250701093631-6125867cea5a/go.mod h1:8vVkpfa+5Xcte2f7YaH6ao8kVZlbz1ErCVGqNanaNLA=
gitea.elara.ws/Hazel/words v1.0.4 h1:pc7RZ0gNYMZ/zeYmY7FZrscZZOO4TEQt5d6SsXLe4zc=
gitea.elara.ws/Hazel/words v1.0.4/go.mod h1:IwQ+eZpY2Kr02RPYpyFDjHNJPPbsf1NOvgc4ZMeg2zg=
gitea.elara.ws/Hazel/words v1.0.5 h1:FjpQezXDPxgNing/DAMJStLT3TC7/dxvCQj3Cg3AJB4=
gitea.elara.ws/Hazel/words v1.0.5/go.mod h1:IwQ+eZpY2Kr02RPYpyFDjHNJPPbsf1NOvgc4ZMeg2zg=
github.com/dustinkirkland/golang-petname v0.0.0-20240428194347-eebcea082ee0 h1:aYo8nnk3ojoQkP5iErif5Xxv0Mo0Ga/FR5+ffl/7+Nk= github.com/dustinkirkland/golang-petname v0.0.0-20240428194347-eebcea082ee0 h1:aYo8nnk3ojoQkP5iErif5Xxv0Mo0Ga/FR5+ffl/7+Nk=
github.com/dustinkirkland/golang-petname v0.0.0-20240428194347-eebcea082ee0/go.mod h1:8AuBTZBRSFqEYBPYULd+NN474/zZBLP+6WeT5S9xlAc= github.com/dustinkirkland/golang-petname v0.0.0-20240428194347-eebcea082ee0/go.mod h1:8AuBTZBRSFqEYBPYULd+NN474/zZBLP+6WeT5S9xlAc=
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/labstack/echo/v4 v4.13.4 h1:oTZZW+T3s9gAu5L8vmzihV7/lkXGZuITzTQkTEhcXEA= github.com/labstack/echo/v4 v4.13.4 h1:oTZZW+T3s9gAu5L8vmzihV7/lkXGZuITzTQkTEhcXEA=
github.com/labstack/echo/v4 v4.13.4/go.mod h1:g63b33BZ5vZzcIUF8AtRH40DrTlXnx4UMC8rBdndmjQ= github.com/labstack/echo/v4 v4.13.4/go.mod h1:g63b33BZ5vZzcIUF8AtRH40DrTlXnx4UMC8rBdndmjQ=
github.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0= github.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0=

View File

@@ -3,6 +3,8 @@ package game
import ( import (
"crypto/ed25519" "crypto/ed25519"
"encoding/base64" "encoding/base64"
"errors"
"strings"
petname "github.com/dustinkirkland/golang-petname" petname "github.com/dustinkirkland/golang-petname"
) )
@@ -16,42 +18,110 @@ type Session struct {
Name string Name string
Users []User Users []User
Phrase []string phrase []string
HiddenPhrase []string AskedLetters []string
DiscoveredPhrase []string
Mistakes int
userIndex int
CurrentUser *User
} }
func NewSession(phrase string) Session { func NewSession(phrase string) *Session {
sessionName := petname.Generate(3, "-") sessionName := petname.Generate(3, "-")
p := strings.Split(phrase, "")
s := Session{ s := Session{
id: lastSessionId, id: lastSessionId,
Name: sessionName, Name: sessionName,
Users: []User{}, Users: make([]User, 0),
phrase: p,
AskedLetters: []string{},
DiscoveredPhrase: make([]string, len(p)),
Mistakes: 0,
userIndex: 0,
CurrentUser: nil,
} }
sessionStorage = append(sessionStorage, s) sessionStorage = append(sessionStorage, s)
nameToSession[sessionName] = &s nameToSession[sessionName] = &sessionStorage[len(sessionStorage)-1]
lastSessionId++ lastSessionId++
return s return &sessionStorage[len(sessionStorage)-1]
} }
func GetSession(name string) *Session { func GetSession(name string) (*Session, error) {
return nameToSession[name] s, ok := nameToSession[name]
if !ok {
return s, errors.New("can't find session " + name)
}
return s, nil
} }
func (s *Session) AddUser(user User) { func (s *Session) AddUser(user User) *User {
s.Users = append(s.Users, user) s.Users = append(s.Users, user)
// fmt.Printf("#### Adding %v:\t%p\n", s.Users[len(s.Users)-1].Name, &(s.Users[len(s.Users)-1]))
// append changes the pointers to the users because it needs to resize that slice
s.CurrentUser = &(s.Users[s.userIndex])
return &(s.Users[len(s.Users)-1])
} }
func (s Session) VerifySignature(signature string, message []byte) *User { func (s *Session) VerifySignature(signature string, message []byte) (*User, error) {
sig, err := base64.StdEncoding.DecodeString(signature)
if err != nil {
return nil, err
}
for i := range s.Users {
// fmt.Printf("looking %v:\t%p\n", s.Users[i].Name, &(s.Users[i]))
if ed25519.Verify(s.Users[i].PublicKey, message, sig) {
return &s.Users[i], nil
}
}
return nil, errors.New("this user was not fount in the current session")
}
func (s *Session) GuessLetter(letter string) (*Session, error) {
letter = strings.ToLower(letter)
if len(letter) != 1 {
return s, errors.New("the letter needs to have a length of one")
}
for _, asked := range s.AskedLetters {
if letter == asked {
return s, errors.New("the letter " + letter + " was already asked")
}
}
s.AskedLetters = append(s.AskedLetters, letter)
found := false
for i, l := range s.phrase {
if l == letter {
found = true
s.DiscoveredPhrase[i] = s.phrase[i]
}
}
if !found {
s.Mistakes++
}
s.userIndex = (s.userIndex + 1) % len(s.Users)
s.CurrentUser = &s.Users[s.userIndex]
return s, nil
}
func (s *Session) GetUserByName(name string) (*User, error) {
for _, u := range s.Users { for _, u := range s.Users {
sig, _ := base64.StdEncoding.DecodeString(signature) if u.Name == name {
return &u, nil
if ed25519.Verify(u.PublicKey, message, sig) {
return &u
} }
} }
return nil return nil, errors.New("nu user with the name " + name + " found in " + s.Name)
} }

View File

@@ -8,12 +8,3 @@ type User struct {
Name string Name string
PublicKey ed25519.PublicKey PublicKey ed25519.PublicKey
} }
func NewUser(name string, publicKey ed25519.PublicKey) User {
// ed25519
return User{
Name: name,
PublicKey: publicKey,
}
}

View File

@@ -4,10 +4,11 @@ import (
"net/http" "net/http"
"gitea.elara.ws/Hazel/hangman/internal/game" "gitea.elara.ws/Hazel/hangman/internal/game"
"gitea.elara.ws/Hazel/words"
"github.com/labstack/echo/v4" "github.com/labstack/echo/v4"
) )
func CreateSession(c echo.Context) error { func CreateSession(c echo.Context) error {
s := game.NewSession() s := game.NewSession(words.Words.GetRandomWord())
return c.JSON(http.StatusOK, s) return c.JSON(http.StatusOK, s)
} }

View File

@@ -9,7 +9,10 @@ import (
) )
func CreateUser(c echo.Context) error { func CreateUser(c echo.Context) error {
session := game.GetSession(c.Param("session")) session, err := game.GetSession(c.Param("session"))
if err != nil {
return c.String(http.StatusBadRequest, err.Error())
}
type BodyContent struct { type BodyContent struct {
Name string Name string
@@ -17,14 +20,23 @@ func CreateUser(c echo.Context) error {
} }
var bodyContent BodyContent var bodyContent BodyContent
err := c.Bind(&bodyContent) err = c.Bind(&bodyContent)
if err != nil { if err != nil {
return c.String(http.StatusBadRequest, err.Error()) return c.String(http.StatusBadRequest, err.Error())
} }
pub, _ := base64.StdEncoding.DecodeString(bodyContent.PublicKey) pub, err := base64.StdEncoding.DecodeString(bodyContent.PublicKey)
user := game.NewUser(bodyContent.Name, pub) if err != nil {
session.AddUser(user) return c.String(http.StatusBadRequest, err.Error())
}
return c.JSON(http.StatusOK, user) user := session.AddUser(game.User{
Name: bodyContent.Name,
PublicKey: pub,
})
return c.JSON(http.StatusOK, ResponseData{
Session: session,
User: user,
})
} }

View File

@@ -0,0 +1,39 @@
package rest_handler
import (
"net/http"
"github.com/labstack/echo/v4"
)
func GuessLetter(c echo.Context) error {
session, user, err := GetData(c)
if err != nil {
return c.String(http.StatusBadRequest, err.Error())
}
if session.CurrentUser != user {
return c.String(http.StatusBadRequest, "It's not the turn of user "+user.Name+". It's the turn of "+session.CurrentUser.Name+".")
}
type BodyContent struct {
Guess string
}
var bodyContent BodyContent
err = c.Bind(&bodyContent)
if err != nil {
return c.String(http.StatusBadRequest, err.Error())
}
_, err = session.GuessLetter(bodyContent.Guess)
if err != nil {
return c.String(http.StatusBadRequest, err.Error())
}
return c.JSON(http.StatusOK, ResponseData{
User: user,
Session: session,
})
}

View File

@@ -0,0 +1,35 @@
package rest_handler
import (
"bytes"
"io"
"gitea.elara.ws/Hazel/hangman/internal/game"
"github.com/labstack/echo/v4"
)
type ResponseData struct{
Session *game.Session
User *game.User
}
func GetData(c echo.Context) (*game.Session, *game.User, error) {
session, err := game.GetSession(c.Param("session"))
if err != nil {
return session, nil, err
}
sig := c.Request().Header.Get("signature")
body, _ := io.ReadAll(c.Request().Body)
user, err := session.VerifySignature(sig, body)
if err != nil {
return session, user, err
}
c.Request().Body = io.NopCloser(bytes.NewBuffer(body))
// fmt.Printf("user %v:\t%p\n", user.Name, user)
return session, user, nil
}

View File

@@ -1,8 +1,6 @@
package rest_handler package rest_handler
import ( import (
"fmt"
"io"
"net/http" "net/http"
"gitea.elara.ws/Hazel/hangman/internal/game" "gitea.elara.ws/Hazel/hangman/internal/game"
@@ -10,30 +8,17 @@ import (
) )
func TestAuth(c echo.Context) error { func TestAuth(c echo.Context) error {
session := game.GetSession(c.Param("session"))
type TestResults struct { type TestResults struct {
SignatureValid bool Session *game.Session
User *string User *game.User
Error error
} }
sig := c.Request().Header.Get("signature") session, user, err := GetData(c)
body, _ := io.ReadAll(c.Request().Body)
fmt.Println(sig)
u := session.VerifySignature(sig, body) return c.JSON(http.StatusOK, TestResults{
var resp TestResults Session: session,
if u == nil { User: user,
resp = TestResults{ Error: err,
SignatureValid: false, })
User: nil,
}
} else {
resp = TestResults{
SignatureValid: true,
User: &u.Name,
}
}
return c.JSON(http.StatusOK, resp)
} }

28
main.go
View File

@@ -1,45 +1,25 @@
package main package main
import ( import (
"errors"
"fmt" "fmt"
"io"
"text/template"
"gitea.elara.ws/Hazel/hangman/internal/rest_handler" "gitea.elara.ws/Hazel/hangman/internal/rest_handler"
"gitea.elara.ws/Hazel/hangman/internal/view_handler" "gitea.elara.ws/Hazel/hangman/internal/view_handler"
"gitea.elara.ws/Hazel/hangman/internal/websocket_handler"
"github.com/labstack/echo/v4" "github.com/labstack/echo/v4"
) )
type TemplateRegistry struct {
templates map[string]*template.Template
}
// Implement e.Renderer interface
func (t *TemplateRegistry) Render(w io.Writer, name string, data interface{}, c echo.Context) error {
tmpl, ok := t.templates[name]
if !ok {
err := errors.New("Template not found -> " + name)
return err
}
return tmpl.ExecuteTemplate(w, "base.html", data)
}
func main() { func main() {
fmt.Println("wanna play hangman? Well ya cant since it isn't implemented yet..") fmt.Println("wanna play hangman? Well ya cant since it isn't implemented yet..")
e := echo.New() e := echo.New()
templates := make(map[string]*template.Template)
templates["create_session"] = template.Must(template.ParseFiles("templates/create_session.html", "templates/base.html"))
templates["create_user"] = template.Must(template.ParseFiles("templates/create_user.html", "templates/base.html"))
e.Renderer = &TemplateRegistry{
templates: templates,
}
e.POST("/api/session", rest_handler.CreateSession) e.POST("/api/session", rest_handler.CreateSession)
e.POST("/api/:session/user", rest_handler.CreateUser) e.POST("/api/:session/user", rest_handler.CreateUser)
e.POST("/api/:session/test-auth", rest_handler.TestAuth) e.POST("/api/:session/test-auth", rest_handler.TestAuth)
e.POST("/api/:session/guess", rest_handler.GuessLetter)
e.GET("/ws/:session/:user", websocket_handler.UserWS)
e.GET("/", view_handler.CreateSession) e.GET("/", view_handler.CreateSession)
e.GET("/:name", view_handler.CreateUser) e.GET("/:name", view_handler.CreateUser)

View File

@@ -36,6 +36,12 @@ class User:
}) })
print(r.content) print(r.content)
def guess(self, letter: str):
r = self.signed_request("/guess", {
"Guess": letter
})
print(r.content)
def signed_request(self, endpoint: str, body: dict) -> requests.Response: def signed_request(self, endpoint: str, body: dict) -> requests.Response:
payload = json.dumps(body).encode("utf-8") payload = json.dumps(body).encode("utf-8")
@@ -51,26 +57,64 @@ class User:
) )
class Session: class Game:
def __init__(self) -> None: def __init__(self) -> None:
self.session_name = input("session: ").strip().lower()
if self.session_name == "":
data = requests.post(BASE_URL + "/session").json() data = requests.post(BASE_URL + "/session").json()
self.name = data["Name"] self.session_name = data["Name"]
self.users: List[User] = []
print(f"playing with session {self.session_name}")
self.user_name = input("name: ").strip()
self.signing_key = SigningKey.generate()
r = requests.post(BASE_URL + f"/{self.session_name}/user", json={
"Name": self.user_name,
"PublicKey": base64.b64encode(self.signing_key.verify_key.__bytes__()).decode("ascii")
})
self.last_turn = r.json()
def __repr__(self) -> str: def __repr__(self) -> str:
return f"Session({self.name})" return f"Game({self.session_name}, {self.user_name})"
def print_turn(self, data: dict):
print()
print(" ".join(c if c != '' else '_' for c in data["Session"]["DiscoveredPhrase"]))
print(f"guessed: {','.join(data['Session']['AskedLetters'])}")
def guess(self):
self.print_turn(self.last_turn)
self.send_guess(input(f"{self.user_name}: ").strip())
def send_guess(self, letter: str):
data = self.signed_request("/guess", {
"Guess": letter
}).json()
self.last_turn = data
def signed_request(self, endpoint: str, body: dict) -> requests.Response:
payload = json.dumps(body).encode("utf-8")
signature = self.signing_key.sign(payload)
return requests.post(
url=BASE_URL+f"/{self.session_name}"+endpoint,
data=payload,
headers={
"Content-Type": "application/json",
"signature": base64.b64encode(signature.signature).decode("ascii")
}
)
def add_user(self, name: str) -> User:
u = User(session=self, name=name)
self.users.append(u)
return u
if __name__ == "__main__": if __name__ == "__main__":
s = Session() g = Game()
print(s)
s.add_user(name="Hazel")
u = s.add_user(name="OtherHazel")
print(u)
u.test_auth() while True:
g.guess()

108
python_frontend/test.py Normal file
View File

@@ -0,0 +1,108 @@
from __future__ import annotations
from typing import List
import requests
from nacl.signing import SigningKey, VerifyKey
import base64
import json
BASE_URL = "http://localhost:1323/api"
class User:
def __init__(self, session: Session, name: str) -> None:
self.session = session
self.name = name
self.signing_key = SigningKey.generate()
r = requests.post(BASE_URL + f"/{self.session.name}/user", json={
"Name": name,
"PublicKey": base64.b64encode(self.signing_key.verify_key.__bytes__()).decode("ascii")
})
data = r.json()
print(json.dumps(data, indent=4))
def __repr__(self) -> str:
return f"User({self.name})"
def test_auth(self):
r = self.signed_request("/test-auth", {
"foo": "bar"
})
print(r.content)
def guess(self, letter: str):
r = self.signed_request("/guess", {
"Guess": letter
})
print(r.content)
def signed_request(self, endpoint: str, body: dict) -> requests.Response:
payload = json.dumps(body).encode("utf-8")
signature = self.signing_key.sign(payload)
return requests.post(
url=BASE_URL+f"/{self.session.name}"+endpoint,
data=payload,
headers={
"Content-Type": "application/json",
"signature": base64.b64encode(signature.signature).decode("ascii")
}
)
class Session:
def __init__(self) -> None:
data = requests.post(BASE_URL + "/session").json()
self.name = data["Name"]
self.users: List[User] = []
def __repr__(self) -> str:
return f"Session({self.name})"
def add_user(self, name: str) -> User:
u = User(session=self, name=name)
self.users.append(u)
return u
def build_word_dict():
lines = [
"""package words
var Words []string = []string{"""
]
with open("/usr/share/dict/words", "r") as f:
for l in f.readlines():
l = l.strip()
if not l.isalpha():
continue
lines.append(f'\t"{l}",')
lines.append("}")
with open("internal/words/dictionary.go", "w") as f:
f.write("\n".join(lines))
exit()
if __name__ == "__main__":
s = Session()
print(s)
a = s.add_user(name="Hazel_1")
b = s.add_user(name="Hazel_2")
c = s.add_user(name="Hazel_3")
a.guess("a")
b.guess("e")
c.guess("i")
a.guess("o")
b.guess("u")