Skip to content

Commit

Permalink
Merge pull request #4 from ahmed-debbech/dev
Browse files Browse the repository at this point in the history
Add seen and solved mechanisms
  • Loading branch information
ahmed-debbech authored Jan 10, 2025
2 parents db41061 + eebc7af commit 8e80342
Show file tree
Hide file tree
Showing 22 changed files with 534 additions and 22 deletions.
Binary file added .demo/demo.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ jobs:
echo "${{ secrets.MongoUrl }}" >> mongo/creds
go get .
go get github.com/ahmed-debbech/go_chess_puzzle/generator/config
go get github.com/ahmed-debbech/go_chess_puzzle/generator/data
go build -v .
ssh ${{ secrets.USERNAME }}@${{ secrets.HOST }} "kill -9 \$(cat ~/backend.pid)" || true
scp backend ${{ secrets.USERNAME }}@${{ secrets.HOST }}:/home/${{ secrets.USERNAME }}
Expand Down
43 changes: 41 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,41 @@
# go_chess_puzzle
A Chess puzzle generator from Lichess.com match databases using Stockfish to calculate best moves.
# Pichess
###### You like Lichess? You will love Pichess ❤️

![](https://github.com/ahmed-debbech/go_chess_puzzle/actions/workflows/deploy.yml/badge.svg)

A Chess puzzle generator from Lichess.org match databases using Stockfish to calculate best moves.

### Demo

![](.demo/demo.gif)

### How does it work?

##### Generation phase:
I built three simple programs:
**1 (Cutter)** - Parses the downloaded file from [Lichess database](https://database.lichess.org/) (a huge text file of several GBs).
The file consists of thousands of PGN games and Cutter is responsible for splitting every game's PGN to a seperate file inside a specified output directory and naming them with unique IDS.
**2 (Generator)** - The main core that produces puzzle candidates.
- Generator selects and opens a PGN file with a given random id (say 15260) from the output directory.
- Generator then reads the PGN and jumps to also a random position played already in the game (say 15).
- At this point Generator Calls stockfish chess engine and tells it "Hey, here is a chess game at the position (say 15) and white plays now, can you finish the game for me?".
- Note that Generator asks stockfish to play at depth 24 and depth 4 so that we can reach to a checkmate.
- Eventually Generator points to the last 2 to 5 moves in the game that is finished by stockfish.
- Now we have a new puzzle candidate

**3 (Storer)** - Stores puzzle candidates coming from generator to MongoDB
Storer accepts any puzzle candidate coming from Generator to save it directly into `puzzles` collection in MongoDB


##### Serving phase:
**The backend**
The actual program that you need to deploy is `backend` that will serve all web pages content and do everything related to checking if client solved or seen a puzzle.
The rest three programs we talked about above are like a toolbox for generating/storing puzzles forbackend to be able to serve them.

### We depend on these amazing libraries...
* [notnil/chess](https://github.com/notnil/chess) library for easier manipulation of chess games in Golang.
* [chessboardjs](https://chessboardjs.com/) the library that draws chess the board you see in your browser.

### Want to contribute?
Please feel free to fork this repo and to open a new pull request with your own modifications.
Contact me at [email protected]
3 changes: 1 addition & 2 deletions backend/TODO
Original file line number Diff line number Diff line change
@@ -1,2 +1 @@
* implement a way to store current unsolved puzzle
* when press New dont refresh page
* implement a way to store current unsolved puzzle
21 changes: 21 additions & 0 deletions backend/TODO1
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
/solved?cid=44-44-444&pid=1234&sh=okokokokkokkookok
1) check the hash received against the ramstore
2) hit mongo with uuid and add it to set

/seen?cid=44-44-444&pid=1234
1) directly hit mongo with uuid and store it as a set

/load
1) set pid to ramstore and calculate hash


hash equation:
exp : d5f7, h1h4, a8g2 [arr]
5*7 + 1*4 + 8*2 = 55 [uniq]
123455d5f7h1h4a8g2 [pid][uniq][arr]
db2bea72fe15509fe830982d9b057f2b4a873b6714e45d5d6430707e24ebc7c3 [hash SHA256]


the ramstore:
key: pid, value: hash

2 changes: 1 addition & 1 deletion backend/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ module github.com/ahmed-debbech/go_chess_puzzle/backend
go 1.23.2

require (
github.com/ahmed-debbech/go_chess_puzzle/generator v0.0.0-20250103134145-8e8a8af4dbd6
github.com/ahmed-debbech/go_chess_puzzle/generator v0.0.0-20250109214724-6b0c7875d733
go.mongodb.org/mongo-driver v1.17.1
)

Expand Down
2 changes: 2 additions & 0 deletions backend/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ github.com/ahmed-debbech/go_chess_puzzle/generator v0.0.0-20241130121936-2161005
github.com/ahmed-debbech/go_chess_puzzle/generator v0.0.0-20241130121936-21610057b122/go.mod h1:O0flTDeoR/1EiJnzsJHM+dYsfhoLLLMprOLa9va+ukQ=
github.com/ahmed-debbech/go_chess_puzzle/generator v0.0.0-20250103134145-8e8a8af4dbd6 h1:nm4DcWjxVBjyZMOhzNNnPfhSMiPO1ZfIveMYYKn0nGg=
github.com/ahmed-debbech/go_chess_puzzle/generator v0.0.0-20250103134145-8e8a8af4dbd6/go.mod h1:h7rdtIN/l62JTh9v0VGX+l62NmKpOceGWZcgNeTUbn0=
github.com/ahmed-debbech/go_chess_puzzle/generator v0.0.0-20250109214724-6b0c7875d733 h1:IPoZo5TpbPFc3/gGnyJzyx7B8GwcdTPK71vpS0rh4YM=
github.com/ahmed-debbech/go_chess_puzzle/generator v0.0.0-20250109214724-6b0c7875d733/go.mod h1:h7rdtIN/l62JTh9v0VGX+l62NmKpOceGWZcgNeTUbn0=
github.com/ajstarks/svgo v0.0.0-20200320125537-f189e35d30ca/go.mod h1:K08gAheRH3/J6wwsYMMT4xOr94bZjxIelGM0+d/wbFw=
github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=
github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
Expand Down
42 changes: 42 additions & 0 deletions backend/logic/puzzle_dto.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package logic

import (
"strconv"
"encoding/json"
"github.com/ahmed-debbech/go_chess_puzzle/generator/data"
"github.com/ahmed-debbech/go_chess_puzzle/generator/config"
)

type PuzzleDto struct {
ID string
FEN string
BestMoves [config.BestMovesNumber]string
GenTime string
CurrentPlayer int
SolveCount int
MatchLink string
SeenCount int
FirstSeenTime string
}

func (p PuzzleDto) String() string{
return p.ID + " FEN: " + p.FEN + " BestMove: " + strconv.Itoa(len(p.BestMoves)) + " GenTime: " + p.GenTime + " CurrentPlayer: "+ strconv.Itoa(p.CurrentPlayer) +" SolveCount: " + strconv.Itoa(p.SolveCount) + " MatchLink: " + p.MatchLink + " SeenCount: " + strconv.Itoa(p.SeenCount) + " FirstSeenTime: " + p.FirstSeenTime;
}

func fromPuzzleDao(puzzleDao *data.Puzzle) *PuzzleDto{
return &PuzzleDto{
ID: puzzleDao.ID,
FEN: puzzleDao.FEN,
BestMoves: puzzleDao.BestMoves,
GenTime: puzzleDao.GenTime,
CurrentPlayer: puzzleDao.CurrentPlayer,
SolveCount: puzzleDao.SolveCount,
MatchLink: puzzleDao.MatchLink,
SeenCount: len(puzzleDao.SeenCount),
FirstSeenTime: puzzleDao.FirstSeenTime,
}
}

func (p PuzzleDto) ToJson() ([]byte, error){
return json.Marshal(p)
}
18 changes: 13 additions & 5 deletions backend/logic/puzzles.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,22 +3,30 @@ package logic
import (
"errors"
"github.com/ahmed-debbech/go_chess_puzzle/backend/mongo"
"github.com/ahmed-debbech/go_chess_puzzle/generator/data"
)


func GetRandomPuzzle() (*data.Puzzle, error){
func GetRandomPuzzle() (*PuzzleDto, error){
dat, err := mongo.MongoFindRandPuzzle()
if err != nil {
return &data.Puzzle{}, errors.New("Could not find a random puzzle.")
return &PuzzleDto{}, errors.New("Could not find a random puzzle.")
}
return dat, nil
pdto := fromPuzzleDao(dat)
return pdto, nil
}

func PuzzleToJson(puzzle data.Puzzle) ([]byte, error){
func PuzzleToJson(puzzle PuzzleDto) ([]byte, error){
dat, err := puzzle.ToJson()
if err != nil {
return []byte{}, errors.New("Could serialize puzzle to JSON")
}
return dat, nil
}

func IncrementSolvedCounter(puzzleId string) {
mongo.IncrementSolved(puzzleId)
}

func MarkPuzzleAsSeen(pid string, uuid string){
mongo.MarkAsSeen(pid, uuid)
}
48 changes: 47 additions & 1 deletion backend/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,10 @@ import (
"embed"
"io/fs"
_"time"
"errors"

"github.com/ahmed-debbech/go_chess_puzzle/backend/logic"
"github.com/ahmed-debbech/go_chess_puzzle/backend/ramstore"
)

//go:embed views/*.html
Expand Down Expand Up @@ -46,9 +49,51 @@ func loadPuzzleHandle(w http.ResponseWriter, r *http.Request){

w.Write([]byte(`{"error": "`+err.Error()+`"}`))
}
ramstore.SetToRamStore(puzzle)
w.Header().Set("Content-Type", "application/json")
w.Write(serial)
}

func solvedHandler(w http.ResponseWriter, r *http.Request){
if(r.Method != "GET"){http.Error(w, "Invalid", http.StatusMethodNotAllowed); return;}

query := r.URL.Query()
puzzleId := query.Get("pid")
hash := query.Get("h")

solved := ramstore.CheckRamStore(puzzleId, hash)

if solved {
go logic.IncrementSolvedCounter(puzzleId)
w.Write([]byte("true"))
}else{
w.Write([]byte("false"))
}
}

func seenHandler(w http.ResponseWriter, r *http.Request) {
if(r.Method != "GET"){http.Error(w, "Invalid", http.StatusMethodNotAllowed); return;}

query := r.URL.Query()
pid := query.Get("pid")

cookie, err := r.Cookie("chess_uuid")
if err != nil {
switch {
case errors.Is(err, http.ErrNoCookie):
http.Error(w, "cookie not found", http.StatusBadRequest)
default:
http.Error(w, "server error", http.StatusInternalServerError)
}
return
}
uuid := cookie.Value

go logic.MarkPuzzleAsSeen(pid, uuid)

w.Write([]byte("seen"))
}

func rootHandle(w http.ResponseWriter, r *http.Request) {
p := loadPage("index.html")
if p == nil { return }
Expand All @@ -74,7 +119,8 @@ func main(){
)
http.HandleFunc("/", rootHandle)
http.HandleFunc("/load", loadPuzzleHandle)

http.HandleFunc("/solved", solvedHandler)
http.HandleFunc("/seen", seenHandler)

fmt.Println(http.ListenAndServe(":5530", nil))
}
30 changes: 30 additions & 0 deletions backend/mongo/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ func MongoFindRandPuzzle() (*data.Puzzle, error){
if len(result) == 0 {return nil, errors.New("could not find any result")}

fmt.Println("[SUCCESS] found random puzzle with id:", result[0].ID)

return &result[0], nil
}

Expand Down Expand Up @@ -89,4 +90,33 @@ func Destroy() {
panic(err)
}
fmt.Println("[SUCCESS] destroy Mongo client")
}

func IncrementSolved(pid string){

coll := client.Database("official").Collection("puzzles")

pipe := bson.D{
{"$inc", bson.D{
{"solvecount", 1},
}},
}
filter := bson.D{{"id", pid}}

_, err := coll.UpdateOne(context.TODO(), filter, pipe)
if err != nil {
fmt.Println("[ERROR] could not increment solvecount for",pid," because:" , err)
}
}
func MarkAsSeen(pid string, uuid string) {
coll := client.Database("official").Collection("puzzles")

pipe := bson.D{{"$addToSet", bson.D{{"seencount", uuid}},}}

filter := bson.D{{"id", pid}}

_, err := coll.UpdateOne(context.TODO(), filter, pipe)
if err != nil {
fmt.Println("[ERROR] could not increment solvecount for",pid," because:" , err)
}
}
84 changes: 84 additions & 0 deletions backend/ramstore/ramdata.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
package ramstore

import (
"fmt"
"strconv"
"strings"
"crypto/sha256"
"encoding/hex"
"errors"

"github.com/ahmed-debbech/go_chess_puzzle/generator/config"
)

type RamStore struct{
store map[string]string
}

var ramStoreInstance *RamStore = nil

func newRamStore() *RamStore{
fmt.Println("Creating new RamStore")
return &RamStore{ store: make(map[string]string) }
}

func GetRamStoreInstance() *RamStore{
if ramStoreInstance == nil {
ramStoreInstance = newRamStore()
}
return ramStoreInstance
}

func Set(pid string, hash string){
ramStoreInstance.store[pid] = hash
fmt.Println(ramStoreInstance)
fmt.Println("LEN: ", len(ramStoreInstance.store))
}

func extractDigits(move string) int{
p := 1
for _, c := range move {
if ('0'<=c) && ('9' >= c) {
ss, _ := strconv.Atoi(string(c))
p *= ss
}
}
return p
}

func doHash(toHash string) string {
hash := sha256.New()
hash.Write([]byte(toHash))
hashBytes := hash.Sum(nil)
hashString := hex.EncodeToString(hashBytes)
return hashString
}

func Calculate(pid string, bestmove [config.BestMovesNumber]string) string{
hash := pid

sum := 0
necessary_moves := make([]string, 0)
for i:=1; i<=len(bestmove)-1; i+=2 {
necessary_moves = append(necessary_moves, bestmove[i])
sum += extractDigits(bestmove[i])
}

hash += strconv.Itoa(sum)
hash += strings.Join(necessary_moves, "")
return doHash(hash)
}

func Get(pid string) (string, error) {

i, ok := ramStoreInstance.store[pid]
if ok {
return i , nil
}
return "", errors.New("hash is not found")

}

func Delete(pid string) {
delete(ramStoreInstance.store, pid)
}
Loading

0 comments on commit 8e80342

Please sign in to comment.