Skip to content

Commit 8e80342

Browse files
Merge pull request #4 from ahmed-debbech/dev
Add seen and solved mechanisms
2 parents db41061 + eebc7af commit 8e80342

File tree

22 files changed

+534
-22
lines changed

22 files changed

+534
-22
lines changed

.demo/demo.gif

1.24 MB
Loading

.github/workflows/deploy.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ jobs:
3232
echo "${{ secrets.MongoUrl }}" >> mongo/creds
3333
go get .
3434
go get github.com/ahmed-debbech/go_chess_puzzle/generator/config
35+
go get github.com/ahmed-debbech/go_chess_puzzle/generator/data
3536
go build -v .
3637
ssh ${{ secrets.USERNAME }}@${{ secrets.HOST }} "kill -9 \$(cat ~/backend.pid)" || true
3738
scp backend ${{ secrets.USERNAME }}@${{ secrets.HOST }}:/home/${{ secrets.USERNAME }}

README.md

Lines changed: 41 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,41 @@
1-
# go_chess_puzzle
2-
A Chess puzzle generator from Lichess.com match databases using Stockfish to calculate best moves.
1+
# Pichess
2+
###### You like Lichess? You will love Pichess ❤️
3+
4+
![](https://github.com/ahmed-debbech/go_chess_puzzle/actions/workflows/deploy.yml/badge.svg)
5+
6+
A Chess puzzle generator from Lichess.org match databases using Stockfish to calculate best moves.
7+
8+
### Demo
9+
10+
![](.demo/demo.gif)
11+
12+
### How does it work?
13+
14+
##### Generation phase:
15+
I built three simple programs:
16+
**1 (Cutter)** - Parses the downloaded file from [Lichess database](https://database.lichess.org/) (a huge text file of several GBs).
17+
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.
18+
**2 (Generator)** - The main core that produces puzzle candidates.
19+
- Generator selects and opens a PGN file with a given random id (say 15260) from the output directory.
20+
- Generator then reads the PGN and jumps to also a random position played already in the game (say 15).
21+
- 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?".
22+
- Note that Generator asks stockfish to play at depth 24 and depth 4 so that we can reach to a checkmate.
23+
- Eventually Generator points to the last 2 to 5 moves in the game that is finished by stockfish.
24+
- Now we have a new puzzle candidate
25+
26+
**3 (Storer)** - Stores puzzle candidates coming from generator to MongoDB
27+
Storer accepts any puzzle candidate coming from Generator to save it directly into `puzzles` collection in MongoDB
28+
29+
30+
##### Serving phase:
31+
**The backend**
32+
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.
33+
The rest three programs we talked about above are like a toolbox for generating/storing puzzles forbackend to be able to serve them.
34+
35+
### We depend on these amazing libraries...
36+
* [notnil/chess](https://github.com/notnil/chess) library for easier manipulation of chess games in Golang.
37+
* [chessboardjs](https://chessboardjs.com/) the library that draws chess the board you see in your browser.
38+
39+
### Want to contribute?
40+
Please feel free to fork this repo and to open a new pull request with your own modifications.
41+
Contact me at debbech.ahmed@gmail.com

backend/TODO

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1 @@
1-
* implement a way to store current unsolved puzzle
2-
* when press New dont refresh page
1+
* implement a way to store current unsolved puzzle

backend/TODO1

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
/solved?cid=44-44-444&pid=1234&sh=okokokokkokkookok
2+
1) check the hash received against the ramstore
3+
2) hit mongo with uuid and add it to set
4+
5+
/seen?cid=44-44-444&pid=1234
6+
1) directly hit mongo with uuid and store it as a set
7+
8+
/load
9+
1) set pid to ramstore and calculate hash
10+
11+
12+
hash equation:
13+
exp : d5f7, h1h4, a8g2 [arr]
14+
5*7 + 1*4 + 8*2 = 55 [uniq]
15+
123455d5f7h1h4a8g2 [pid][uniq][arr]
16+
db2bea72fe15509fe830982d9b057f2b4a873b6714e45d5d6430707e24ebc7c3 [hash SHA256]
17+
18+
19+
the ramstore:
20+
key: pid, value: hash
21+

backend/go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ module github.com/ahmed-debbech/go_chess_puzzle/backend
33
go 1.23.2
44

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

backend/go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ github.com/ahmed-debbech/go_chess_puzzle/generator v0.0.0-20241130121936-2161005
44
github.com/ahmed-debbech/go_chess_puzzle/generator v0.0.0-20241130121936-21610057b122/go.mod h1:O0flTDeoR/1EiJnzsJHM+dYsfhoLLLMprOLa9va+ukQ=
55
github.com/ahmed-debbech/go_chess_puzzle/generator v0.0.0-20250103134145-8e8a8af4dbd6 h1:nm4DcWjxVBjyZMOhzNNnPfhSMiPO1ZfIveMYYKn0nGg=
66
github.com/ahmed-debbech/go_chess_puzzle/generator v0.0.0-20250103134145-8e8a8af4dbd6/go.mod h1:h7rdtIN/l62JTh9v0VGX+l62NmKpOceGWZcgNeTUbn0=
7+
github.com/ahmed-debbech/go_chess_puzzle/generator v0.0.0-20250109214724-6b0c7875d733 h1:IPoZo5TpbPFc3/gGnyJzyx7B8GwcdTPK71vpS0rh4YM=
8+
github.com/ahmed-debbech/go_chess_puzzle/generator v0.0.0-20250109214724-6b0c7875d733/go.mod h1:h7rdtIN/l62JTh9v0VGX+l62NmKpOceGWZcgNeTUbn0=
79
github.com/ajstarks/svgo v0.0.0-20200320125537-f189e35d30ca/go.mod h1:K08gAheRH3/J6wwsYMMT4xOr94bZjxIelGM0+d/wbFw=
810
github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=
911
github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=

backend/logic/puzzle_dto.go

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
package logic
2+
3+
import (
4+
"strconv"
5+
"encoding/json"
6+
"github.com/ahmed-debbech/go_chess_puzzle/generator/data"
7+
"github.com/ahmed-debbech/go_chess_puzzle/generator/config"
8+
)
9+
10+
type PuzzleDto struct {
11+
ID string
12+
FEN string
13+
BestMoves [config.BestMovesNumber]string
14+
GenTime string
15+
CurrentPlayer int
16+
SolveCount int
17+
MatchLink string
18+
SeenCount int
19+
FirstSeenTime string
20+
}
21+
22+
func (p PuzzleDto) String() string{
23+
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;
24+
}
25+
26+
func fromPuzzleDao(puzzleDao *data.Puzzle) *PuzzleDto{
27+
return &PuzzleDto{
28+
ID: puzzleDao.ID,
29+
FEN: puzzleDao.FEN,
30+
BestMoves: puzzleDao.BestMoves,
31+
GenTime: puzzleDao.GenTime,
32+
CurrentPlayer: puzzleDao.CurrentPlayer,
33+
SolveCount: puzzleDao.SolveCount,
34+
MatchLink: puzzleDao.MatchLink,
35+
SeenCount: len(puzzleDao.SeenCount),
36+
FirstSeenTime: puzzleDao.FirstSeenTime,
37+
}
38+
}
39+
40+
func (p PuzzleDto) ToJson() ([]byte, error){
41+
return json.Marshal(p)
42+
}

backend/logic/puzzles.go

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,22 +3,30 @@ package logic
33
import (
44
"errors"
55
"github.com/ahmed-debbech/go_chess_puzzle/backend/mongo"
6-
"github.com/ahmed-debbech/go_chess_puzzle/generator/data"
76
)
87

98

10-
func GetRandomPuzzle() (*data.Puzzle, error){
9+
func GetRandomPuzzle() (*PuzzleDto, error){
1110
dat, err := mongo.MongoFindRandPuzzle()
1211
if err != nil {
13-
return &data.Puzzle{}, errors.New("Could not find a random puzzle.")
12+
return &PuzzleDto{}, errors.New("Could not find a random puzzle.")
1413
}
15-
return dat, nil
14+
pdto := fromPuzzleDao(dat)
15+
return pdto, nil
1616
}
1717

18-
func PuzzleToJson(puzzle data.Puzzle) ([]byte, error){
18+
func PuzzleToJson(puzzle PuzzleDto) ([]byte, error){
1919
dat, err := puzzle.ToJson()
2020
if err != nil {
2121
return []byte{}, errors.New("Could serialize puzzle to JSON")
2222
}
2323
return dat, nil
24+
}
25+
26+
func IncrementSolvedCounter(puzzleId string) {
27+
mongo.IncrementSolved(puzzleId)
28+
}
29+
30+
func MarkPuzzleAsSeen(pid string, uuid string){
31+
mongo.MarkAsSeen(pid, uuid)
2432
}

backend/main.go

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,10 @@ import (
66
"embed"
77
"io/fs"
88
_"time"
9+
"errors"
10+
911
"github.com/ahmed-debbech/go_chess_puzzle/backend/logic"
12+
"github.com/ahmed-debbech/go_chess_puzzle/backend/ramstore"
1013
)
1114

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

4750
w.Write([]byte(`{"error": "`+err.Error()+`"}`))
4851
}
52+
ramstore.SetToRamStore(puzzle)
4953
w.Header().Set("Content-Type", "application/json")
5054
w.Write(serial)
5155
}
56+
57+
func solvedHandler(w http.ResponseWriter, r *http.Request){
58+
if(r.Method != "GET"){http.Error(w, "Invalid", http.StatusMethodNotAllowed); return;}
59+
60+
query := r.URL.Query()
61+
puzzleId := query.Get("pid")
62+
hash := query.Get("h")
63+
64+
solved := ramstore.CheckRamStore(puzzleId, hash)
65+
66+
if solved {
67+
go logic.IncrementSolvedCounter(puzzleId)
68+
w.Write([]byte("true"))
69+
}else{
70+
w.Write([]byte("false"))
71+
}
72+
}
73+
74+
func seenHandler(w http.ResponseWriter, r *http.Request) {
75+
if(r.Method != "GET"){http.Error(w, "Invalid", http.StatusMethodNotAllowed); return;}
76+
77+
query := r.URL.Query()
78+
pid := query.Get("pid")
79+
80+
cookie, err := r.Cookie("chess_uuid")
81+
if err != nil {
82+
switch {
83+
case errors.Is(err, http.ErrNoCookie):
84+
http.Error(w, "cookie not found", http.StatusBadRequest)
85+
default:
86+
http.Error(w, "server error", http.StatusInternalServerError)
87+
}
88+
return
89+
}
90+
uuid := cookie.Value
91+
92+
go logic.MarkPuzzleAsSeen(pid, uuid)
93+
94+
w.Write([]byte("seen"))
95+
}
96+
5297
func rootHandle(w http.ResponseWriter, r *http.Request) {
5398
p := loadPage("index.html")
5499
if p == nil { return }
@@ -74,7 +119,8 @@ func main(){
74119
)
75120
http.HandleFunc("/", rootHandle)
76121
http.HandleFunc("/load", loadPuzzleHandle)
77-
122+
http.HandleFunc("/solved", solvedHandler)
123+
http.HandleFunc("/seen", seenHandler)
78124

79125
fmt.Println(http.ListenAndServe(":5530", nil))
80126
}

backend/mongo/client.go

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ func MongoFindRandPuzzle() (*data.Puzzle, error){
6262
if len(result) == 0 {return nil, errors.New("could not find any result")}
6363

6464
fmt.Println("[SUCCESS] found random puzzle with id:", result[0].ID)
65+
6566
return &result[0], nil
6667
}
6768

@@ -89,4 +90,33 @@ func Destroy() {
8990
panic(err)
9091
}
9192
fmt.Println("[SUCCESS] destroy Mongo client")
93+
}
94+
95+
func IncrementSolved(pid string){
96+
97+
coll := client.Database("official").Collection("puzzles")
98+
99+
pipe := bson.D{
100+
{"$inc", bson.D{
101+
{"solvecount", 1},
102+
}},
103+
}
104+
filter := bson.D{{"id", pid}}
105+
106+
_, err := coll.UpdateOne(context.TODO(), filter, pipe)
107+
if err != nil {
108+
fmt.Println("[ERROR] could not increment solvecount for",pid," because:" , err)
109+
}
110+
}
111+
func MarkAsSeen(pid string, uuid string) {
112+
coll := client.Database("official").Collection("puzzles")
113+
114+
pipe := bson.D{{"$addToSet", bson.D{{"seencount", uuid}},}}
115+
116+
filter := bson.D{{"id", pid}}
117+
118+
_, err := coll.UpdateOne(context.TODO(), filter, pipe)
119+
if err != nil {
120+
fmt.Println("[ERROR] could not increment solvecount for",pid," because:" , err)
121+
}
92122
}

backend/ramstore/ramdata.go

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
package ramstore
2+
3+
import (
4+
"fmt"
5+
"strconv"
6+
"strings"
7+
"crypto/sha256"
8+
"encoding/hex"
9+
"errors"
10+
11+
"github.com/ahmed-debbech/go_chess_puzzle/generator/config"
12+
)
13+
14+
type RamStore struct{
15+
store map[string]string
16+
}
17+
18+
var ramStoreInstance *RamStore = nil
19+
20+
func newRamStore() *RamStore{
21+
fmt.Println("Creating new RamStore")
22+
return &RamStore{ store: make(map[string]string) }
23+
}
24+
25+
func GetRamStoreInstance() *RamStore{
26+
if ramStoreInstance == nil {
27+
ramStoreInstance = newRamStore()
28+
}
29+
return ramStoreInstance
30+
}
31+
32+
func Set(pid string, hash string){
33+
ramStoreInstance.store[pid] = hash
34+
fmt.Println(ramStoreInstance)
35+
fmt.Println("LEN: ", len(ramStoreInstance.store))
36+
}
37+
38+
func extractDigits(move string) int{
39+
p := 1
40+
for _, c := range move {
41+
if ('0'<=c) && ('9' >= c) {
42+
ss, _ := strconv.Atoi(string(c))
43+
p *= ss
44+
}
45+
}
46+
return p
47+
}
48+
49+
func doHash(toHash string) string {
50+
hash := sha256.New()
51+
hash.Write([]byte(toHash))
52+
hashBytes := hash.Sum(nil)
53+
hashString := hex.EncodeToString(hashBytes)
54+
return hashString
55+
}
56+
57+
func Calculate(pid string, bestmove [config.BestMovesNumber]string) string{
58+
hash := pid
59+
60+
sum := 0
61+
necessary_moves := make([]string, 0)
62+
for i:=1; i<=len(bestmove)-1; i+=2 {
63+
necessary_moves = append(necessary_moves, bestmove[i])
64+
sum += extractDigits(bestmove[i])
65+
}
66+
67+
hash += strconv.Itoa(sum)
68+
hash += strings.Join(necessary_moves, "")
69+
return doHash(hash)
70+
}
71+
72+
func Get(pid string) (string, error) {
73+
74+
i, ok := ramStoreInstance.store[pid]
75+
if ok {
76+
return i , nil
77+
}
78+
return "", errors.New("hash is not found")
79+
80+
}
81+
82+
func Delete(pid string) {
83+
delete(ramStoreInstance.store, pid)
84+
}

0 commit comments

Comments
 (0)