Skip to content

Commit

Permalink
Load Offline Puzzles (#19)
Browse files Browse the repository at this point in the history
Load puzzles from a CSV that has been generated via the Lichess Puzzle
Database. Only a small selection of puzzles have been selected from the
entire database. Roughly 10 of every puzzle rating, based on popularity.

LocalStorage is being used to save player's rating in their browser.
Currently just starting at 400 and increasing / decreasing as puzzles
are solved. Using a very basic ELO calculation with 32 max movement.

Closes #5
  • Loading branch information
FeXd committed May 7, 2023
2 parents 17f6613 + 939386b commit 84151a9
Show file tree
Hide file tree
Showing 6 changed files with 24,857 additions and 22 deletions.
1 change: 1 addition & 0 deletions CNAME
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
puzzlechess.ca
2 changes: 2 additions & 0 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,8 @@ <h1>Puzzle ♟ Chess</h1>
<p class="attribution">Code available on <a href="https://github.com/FeXd/puzzle-chess"
target="_blank">GitHub</a> | Play on <a href="https://fexd.itch.io/puzzle-chess">itch.io</a></p>
</footer>

<aside id="debug"></aside>
</body>

</html>
146 changes: 146 additions & 0 deletions puzzles/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
# Puzzles

The puzzles in this directory were downloaded from the [Lichess Open Database](https://database.lichess.org/#puzzles), split into files based on rating, and stripped of unused data.

## Online

Online files contain puzzle id and rating only. The IDs are used to query the [Lichess API](https://lichess.org/api#tag/Puzzles/operation/apiPuzzleId). Deciding which ID to fetch is based on the player's current rating.

### Example Query

`https://lichess.org/api/puzzle/K69di`

### Example Response

```json
{
"game": {
"clock": "10+0",
"id": "VpVdGbna",
"perf": {
"key": "rapid",
"name": "Rapid"
},
"pgn": "d4 Nf6 Nf3 g6 Nc3 d6 e4 c5 Be3 cxd4 Bxd4 Nc6 Be3 Qa5 Bd2 Bg7 Be2 O-O O-O Qb6 Rb1 Bg4 h3 Bxf3 Bxf3 Nd4 Be3 Nxf3+ Qxf3 Qc6 Bd4 a6 Bxf6 Bxf6 Nd5 Qxc2 Nxf6+ exf6 Qxf6 Qxe4 Qxd6 Rad8 Qb6 Rfe8 Rfe1 Qxe1+ Rxe1 Rxe1+ Kh2 Rd2 Kg3 Ree2 Qxb7 Rxb2 Qxa6 Rxa2 Qc8+ Kg7 Qc3+ Kg8 Qc5 Rxf2 Qc8+ Kg7 Qc3+ Kh6 Qe3+ Kg7 Qe5+ Kf8 Qh8+ Ke7 Qe5+ Kf8 Qb8+ Kg7 Qe5+ f6 Qe7+ Kh6 Qf8+ Kg5 h4+ Kh5 Qc5+ f5 Qc1 Rxg2+ Kh3 Rh2+ Kg3 Rag2+ Kf3 Rg4 Qd1 Rhxh4 Kf2 Rh2+ Kf3 Rh3+ Ke2 Rg2+ Kf1+ Rg4 Kf2 g5 Qd8 h6 Qe8+ Kh4 Kf1 h5 Qe1+ Rhg3 Qe5 f4 Qe1 f3 Kf2 Rf4 Qh1+ Rh3 Qe1 g4",
"players": [
{
"color": "white",
"name": "borska (2013)",
"userId": "borska"
},
{
"color": "black",
"name": "Xxn00bkillar69xX (1990)",
"userId": "xxn00bkillar69xx"
}
],
"rated": true
},
"puzzle": {
"id": "K69di",
"initialPly": 123,
"plays": 1970,
"rating": 2022,
"solution": [
"e1e7",
"f4f6",
"e7f6"
],
"themes": [
"short",
"queenRookEndgame",
"endgame",
"mateIn2"
]
}
}
```

## Offline

It is important for this project to support offline puzzles. Only minimal information is stored to keep file size small: puzzle id, fen, moves, rating. As a first "quick and dirty" solution to keeping file sizes small, puzzles were sorted into files in rating intervals of 100. This created files rougly less than 2MB each.

Note that these offline puzzles are designed to be used when fetch requests fail from the Lichess API. This is one of the primary reasons breaking the puzzles up into smaller files.

## Generating Puzzle Files

A simple Python script was used to generate these files from the original Lichess csv file. It is pasted below. Why not upload the file in a .py file? Mostly to not accidentally flag the repository with a python label. Why not use node.js? Because... I said so.

```python
import csv

# Define the rating rounding interval and output file names
rating_interval = 100
output_files = {}
for i in range(1, 31):
filename = '{}00.csv'.format(i)
output_files[filename] = ((i - 1) * rating_interval, i * rating_interval - 1)

# Open all the output files and create a CSV writer for each file
writers = {}
for filename in output_files.keys():
file = open(filename, 'w')
writer = csv.writer(file)
writers[filename] = (file, writer)

# Loop through each row in the input file
with open('input.csv', 'r') as input_file:
reader = csv.reader(input_file)

for row in reader:
rating = int(row[3])
rating_range = None

# Determine which output file to write to based on the rating
for filename, (min_rating, max_rating) in output_files.items():
if rating >= min_rating and rating <= max_rating:
rating_range = (min_rating, max_rating)
break

# Write the selected columns to the output file
if rating_range:
filename = '{}00.csv'.format(int(rating_range[0] / rating_interval) + 1)
writers[filename][1].writerow([row[0], row[3]])

# Close all the output files
for file, writer in writers.values():
file.close()

```

## Generating puzzles.csv

For Offline play it makes more sense for now to just include the most popular puzzles. Below is a script that will go through all of the puzzles in the lichess .csv and output `puzzles.csv` that includes the 10 most popular puzzles from each rating:

```python
input_file = 'lichess_db_puzzle.csv'
output_file = 'puzzles.csv'

# Read CSV file and store the puzzles in a dictionary grouped by rating
puzzles_by_rating = {}
with open(input_file, 'r') as csvfile:
for line in csvfile:
row = line.strip().split(',')
puzzle_id, fen, moves, rating, popularity = row[0], row[1], row[2], int(row[3]), int(row[5])
puzzle = {'puzzle_id': puzzle_id, 'fen': fen, 'moves': moves, 'rating': rating, 'popularity': popularity}

if rating not in puzzles_by_rating:
puzzles_by_rating[rating] = []

puzzles_by_rating[rating].append(puzzle)

# Select the top 10 most popular puzzles for each rating
selected_puzzles = []
for rating, puzzles in puzzles_by_rating.items():
top_puzzles = sorted(puzzles, key=lambda p: p['popularity'], reverse=True)[:10]
selected_puzzles.extend(top_puzzles)

# Write the selected puzzles to the output CSV file
with open(output_file, 'w') as csvfile:
for puzzle in selected_puzzles:
csvfile.write(','.join([puzzle['puzzle_id'], puzzle['fen'], puzzle['moves'], str(puzzle['rating'])]) + '\n')
```

## Future

There are many other ways to optimize these files, but this is a quick and simple way to get it done. If file size becomes an issue, some form of compression would really improve things.
Loading

0 comments on commit 84151a9

Please sign in to comment.