Skip to content

Commit 420eb82

Browse files
authored
Merge pull request #41 from cyrillkuettel/export-pgn
Add PGN export for search
2 parents a2b3816 + dbf0d73 commit 420eb82

File tree

9 files changed

+104
-1
lines changed

9 files changed

+104
-1
lines changed

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ iced_aw = { version = "0.11.0", default-features = false, features = ["tabs"] }
1212
iced_drop = {git = "https://github.com/jhannyj/iced_drop.git", rev="d259ec4dff098852d995d3bcaa5551a88330636f"}
1313

1414
rand = "0.8.5"
15+
chrono = "0.4.41"
1516
chess = "3.2.0"
1617
csv = "1.3.1"
1718
serde = "1.0.217"

src/export.rs

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ use std::str::FromStr;
33
use lopdf::dictionary;
44
use lopdf::{Document, Object, Stream};
55
use lopdf::content::{Content, Operation};
6+
use chrono;
67
use chess::{Board, ChessMove, Color, Piece, Square};
78

89
use crate::{config, PuzzleTab, lang};
@@ -343,3 +344,89 @@ fn gen_diagram_operations(index: usize, puzzle: &config::Puzzle, start_x:i32, st
343344
ops
344345
}
345346

347+
pub fn to_pgn(puzzles: &Vec<config::Puzzle>, lang: &lang::Language, path: String) {
348+
let mut pgn_content = String::new();
349+
350+
for (puzzle_index, puzzle) in puzzles.iter().enumerate() {
351+
// Start with a board from the FEN
352+
let mut board = Board::from_str(&puzzle.fen).unwrap();
353+
354+
// Add PGN headers
355+
pgn_content.push_str(&format!("[Event \"Chess Puzzle\"]\n"));
356+
pgn_content.push_str(&format!("[Site \"https://lichess.org/training/{}\"]\n", puzzle.puzzle_id));
357+
pgn_content.push_str(&format!("[Date \"{}\"]\n", chrono::Local::now().format("%Y.%m.%d")));
358+
pgn_content.push_str(&format!("[White \"{}\"]\n", if board.side_to_move() == Color::White { "Player" } else { "Opponent" }));
359+
pgn_content.push_str(&format!("[Black \"{}\"]\n", if board.side_to_move() == Color::Black { "Player" } else { "Opponent" }));
360+
pgn_content.push_str(&format!("[Result \"*\"]\n"));
361+
pgn_content.push_str(&format!("[GameID \"{}\"]\n", puzzle.game_url));
362+
pgn_content.push_str(&format!("[FEN \"{}\"]\n", puzzle.fen));
363+
pgn_content.push_str(&format!("[SetUp \"1\"]\n"));
364+
if !puzzle.opening.is_empty() {
365+
pgn_content.push_str(&format!("[Opening \"{}\"]\n", puzzle.opening));
366+
}
367+
// Add puzzle details
368+
pgn_content.push_str(&format!("[PuzzleRating \"{}\"]\n", puzzle.rating));
369+
pgn_content.push_str(&format!("[PuzzleRatingDeviation \"{}\"]\n", puzzle.rating_deviation));
370+
pgn_content.push_str(&format!("[PuzzlePopularity \"{}\"]\n", puzzle.popularity));
371+
pgn_content.push_str(&format!("[PuzzleNbPlays \"{}\"]\n", puzzle.nb_plays));
372+
pgn_content.push_str(&format!("[PuzzleThemes \"{}\"]\n", puzzle.themes));
373+
374+
// Start the move list
375+
let puzzle_moves: Vec<&str> = puzzle.moves.split_whitespace().collect();
376+
let mut move_number = 1;
377+
let mut is_white_to_move = board.side_to_move() == Color::White;
378+
379+
// Process the first move (opponent's move that sets up the puzzle)
380+
let first_move = puzzle_moves[0];
381+
let movement = ChessMove::new(
382+
Square::from_str(&String::from(&first_move[..2])).unwrap(),
383+
Square::from_str(&String::from(&first_move[2..4])).unwrap(),
384+
PuzzleTab::check_promotion(first_move)
385+
);
386+
387+
let san_move = config::coord_to_san(&board, String::from(first_move), lang).unwrap();
388+
389+
if is_white_to_move {
390+
pgn_content.push_str(&format!("{}. {}", move_number, san_move));
391+
} else {
392+
pgn_content.push_str(&format!("{}... {}", move_number, san_move));
393+
move_number += 1;
394+
}
395+
396+
// Apply the move to the board
397+
board = board.make_move_new(movement);
398+
is_white_to_move = !is_white_to_move;
399+
400+
// Process the rest of the moves (the actual puzzle solution)
401+
for chess_move in puzzle_moves.iter().skip(1) {
402+
if is_white_to_move {
403+
pgn_content.push_str(&format!(" {}. ", move_number));
404+
} else {
405+
pgn_content.push_str(" ");
406+
}
407+
408+
let san_move = config::coord_to_san(&board, String::from(*chess_move), lang).unwrap();
409+
pgn_content.push_str(&san_move);
410+
411+
// Apply the move to the board
412+
let movement = ChessMove::new(
413+
Square::from_str(&String::from(&chess_move[..2])).unwrap(),
414+
Square::from_str(&String::from(&chess_move[2..4])).unwrap(),
415+
PuzzleTab::check_promotion(chess_move)
416+
);
417+
board = board.make_move_new(movement);
418+
419+
if !is_white_to_move {
420+
move_number += 1;
421+
}
422+
is_white_to_move = !is_white_to_move;
423+
}
424+
425+
// End the game with a result
426+
pgn_content.push_str(" *\n\n");
427+
}
428+
429+
// Write to file
430+
std::fs::write(path, pgn_content).expect("Unable to write PGN file");
431+
}
432+

src/main.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,7 @@ pub enum Message {
107107
SaveScreenshot(Option<(Screenshot, String)>),
108108
ExportPDF(Option<String>),
109109
LoadPuzzle(Option<Vec<config::Puzzle>>),
110+
ExportPGN(Option<String>),
110111
ChangeSettings(Option<config::OfflinePuzzlesConfig>),
111112
EventOccurred(iced::Event),
112113
StartEngine,
@@ -666,6 +667,11 @@ impl OfflinePuzzles {
666667
export::to_pdf(&self.puzzle_tab.puzzles, self.settings_tab.export_pgs.parse::<i32>().unwrap(), &self.lang, file_path);
667668
}
668669
Task::none()
670+
} (_, Message::ExportPGN(file_path)) => {
671+
if let Some(file_path) = file_path {
672+
export::to_pgn(&self.puzzle_tab.puzzles, &self.lang, file_path);
673+
}
674+
Task::none()
669675
} (_, Message::EventOccurred(event)) => {
670676
if let Event::Window(window::Event::CloseRequested) = event {
671677
match self.engine_state {

src/puzzles.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ pub enum PuzzleMessage {
1414
OpenLink(String),
1515
TakeScreenshot,
1616
ExportToPDF,
17+
ExportToPGN
1718
}
1819

1920
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
@@ -60,6 +61,8 @@ impl PuzzleTab {
6061
iced::window::screenshot(self.window_id.unwrap()).map(Message::ScreenshotCreated)
6162
} PuzzleMessage::ExportToPDF => {
6263
Task::perform(PuzzleTab::export(), Message::ExportPDF)
64+
} PuzzleMessage::ExportToPGN => {
65+
return Task::perform(PuzzleTab::export(), Message::ExportPGN);
6366
}
6467
}
6568
}
@@ -133,6 +136,7 @@ impl Tab for PuzzleTab {
133136
],
134137
Button::new(Text::new(lang::tr(&self.lang, "screenshot"))).on_press(PuzzleMessage::TakeScreenshot),
135138
Button::new(Text::new(lang::tr(&self.lang, "export_pdf_btn"))).on_press(PuzzleMessage::ExportToPDF),
139+
Button::new(Text::new(lang::tr(&self.lang, "export_pgn"))).padding(5).on_press(PuzzleMessage::ExportToPGN),
136140
].padding([0, 30]).spacing(10).align_x(Alignment::Center))
137141
} else {
138142
Scrollable::new(col![

src/settings.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,8 @@ impl SettingsTab {
127127
self.export_pgs = String::from("0");
128128
}
129129
Task::none()
130-
} SettingsMessage::ChangePressed => {
130+
},
131+
SettingsMessage::ChangePressed => {
131132
let engine_path = if self.engine_path.is_empty() {
132133
None
133134
} else {

translations/en-US/ocp.ftl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ show_coords = Show coordinates:
7878
pdf_number_of_pages = No. of pages to export to PDF:
7979
get_first_puzzles1 = Get the first
8080
get_first_puzzles2 = {" "}puzzles
81+
export_pgn = Export current puzzles as PGN
8182
engine_path = Engine path (with .exe name):
8283
save = Save Changes
8384
settings_saved = Settings saved!

translations/es/ocp.ftl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ show_coords = Coordenadas del tablero:
7878
pdf_number_of_pages = N. de páginas para exportar en PDF:
7979
get_first_puzzles1 = Obtener los primeros
8080
get_first_puzzles2 = {" "}ejercícios
81+
export_pgn = Exportar ejercicios actuales a PGN
8182
engine_path = Camino del motor de ajedrez (con el nombre del .exe):
8283
save = Guardar Cambios
8384
settings_saved = Preferencias guardadas!

translations/fr/ocp.ftl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@ show_coords = Montrer les coordonnées:
8080
pdf_number_of_pages = Limite de pages pour le PDF:
8181
get_first_puzzles1 = Accéder aux
8282
get_first_puzzles2 = {" "}premiers puzzles
83+
export_pgn = Exporter les puzzles actuels en PGN
8384
engine_path = Chemin d'accès du moteur (avec le nom du fichier .exe):
8485
save = Enregistrer les modifications
8586
settings_saved = Paramètres enregistrés !

translations/pt-BR/ocp.ftl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ show_coords = Coordenadas do tabuleiro:
7878
pdf_number_of_pages = N. de pags. para exportar em PDF:
7979
get_first_puzzles1 = Obter os primeiros
8080
get_first_puzzles2 = {" "}problemas
81+
export_pgn = Exportar problemas atuais para PGN
8182
engine_path = Caminho para a engine (com o .exe):
8283
save = Salvar Mudanças
8384
settings_saved = Configurações salvas!

0 commit comments

Comments
 (0)