use std::io::Write;
use itertools::Itertools;
use super::super::file_format::FileFormatWriter;
use super::super::{IoWriteError, WriterSettings};
use super::helpers::{
AUTHOR, BOXORDER, COLLECTION, DATE_CREATED, DATE_OF_LAST_CHANGE, GOALORDER, RAW_FILE_NOTES,
SOLVER, VARIANT,
};
use crate::rle::Rle;
use crate::utilities::is_blank;
use crate::{Collection, Puzzle, Tessellation};
const SOKOBAN_VARIANT: &str = "Sokoban";
const HEXOBAN_VARIANT: &str = "Hexoban";
const TRIOBAN_VARIANT: &str = "Trioban";
const OCTOBAN_VARIANT: &str = "Octoban";
const fn variant_name(tessellation: Tessellation) -> &'static str {
match tessellation {
Tessellation::Sokoban => SOKOBAN_VARIANT,
Tessellation::Trioban => TRIOBAN_VARIANT,
Tessellation::Hexoban => HEXOBAN_VARIANT,
Tessellation::Octoban => OCTOBAN_VARIANT,
}
}
pub struct SokWriter {
restrict_tessellation: Option<Tessellation>,
suppress_extra: bool,
settings: WriterSettings,
}
impl FileFormatWriter for SokWriter {
fn write_puzzle<W>(
&self, puzzle: &Puzzle, puzzle_idx: usize, dest: &mut W,
) -> Result<(), IoWriteError>
where
W: Write,
{
self.puzzle_writer(puzzle, dest, true, puzzle_idx)
}
fn write_collection<W>(&self, collection: &Collection, dest: &mut W) -> Result<(), IoWriteError>
where
W: Write,
{
self.write_collection_header(collection, dest)?;
for (idx, puzzle) in collection.puzzles.iter().enumerate() {
self.puzzle_writer(
puzzle,
dest,
!is_blank(&puzzle.author) && puzzle.author != collection.author,
idx,
)?;
}
Ok(())
}
}
impl SokWriter {
pub fn new(
restrict_tessellation: Option<Tessellation>, suppress_extra: bool, settings: WriterSettings,
) -> Self {
Self {
restrict_tessellation,
suppress_extra,
settings,
}
}
fn write_saved_game<W>(
&self, dest: &mut W, puzzle: &Puzzle, idx: usize,
) -> Result<(), IoWriteError>
where
W: Write,
{
let saved_game = &puzzle.saved_games[idx];
if saved_game.is_empty() {
return Ok(());
}
if is_blank(&saved_game.title) {
let pushes_count = saved_game.pushes_count();
writeln!(
dest,
"Save game {}/{}",
saved_game.moves_count() + pushes_count,
pushes_count
)?;
} else {
writeln!(dest, "{}", saved_game.title)?;
}
let steps_str = saved_game.printed_steps();
let moves = if self.settings.rle_encode {
Rle::encode(&steps_str).map_err(|e| IoWriteError::new_rle_encoding_error(e, idx))?
} else {
steps_str
.chars()
.chunks(usize::from(self.settings.wrap_savegame_lines_at))
.into_iter()
.map(std::iter::Iterator::collect::<String>)
.join("\n")
};
writeln!(dest, "{moves}")?;
writeln!(dest)?;
let mut written = false;
if let Some(s) = Self::construct_tagged(SOLVER, &saved_game.solved_by) {
writeln!(dest, "{s}")?;
written = true;
}
if !is_blank(&saved_game.notes) {
writeln!(dest, "{}", saved_game.notes.trim_end())?;
written = true;
}
if written {
writeln!(dest)?;
}
Ok(())
}
fn puzzle_writer<W>(
&self, puzzle: &Puzzle, dest: &mut W, write_author: bool, puzzle_idx: usize,
) -> Result<(), IoWriteError>
where
W: Write,
{
if let Some(restricted) = self.restrict_tessellation
&& restricted != puzzle.board.tessellation()
{
return Err(IoWriteError::new_variant_not_allowed_error(
puzzle_idx,
puzzle.board.tessellation(),
));
}
if puzzle.board.is_blank() {
return Ok(());
}
if is_blank(&puzzle.title) {
writeln!(dest, "Level {}", puzzle_idx + 1)?;
} else {
writeln!(dest, "{}", puzzle.title)?;
}
writeln!(dest)?;
writeln!(
dest,
"{}",
puzzle
.board
.printed(self.settings.print_visible_floor)
.trim_end()
)?;
writeln!(dest)?;
if !self.suppress_extra {
let mut written = false;
if puzzle.board.tessellation() != Tessellation::Sokoban
&& let Some(s) =
Self::construct_tagged(VARIANT, variant_name(puzzle.board.tessellation()))
{
writeln!(dest, "{s}")?;
written = true;
}
if let Some(sokoban_plus) = &puzzle.sokoban_plus
&& !sokoban_plus.is_empty()
{
if let Some(s) = Self::construct_tagged(BOXORDER, &sokoban_plus.boxorder()) {
writeln!(dest, "{s}")?;
}
if let Some(s) = Self::construct_tagged(GOALORDER, &sokoban_plus.goalorder()) {
writeln!(dest, "{s}")?;
}
written = true;
}
if write_author {
if let Some(s) = Self::construct_tagged(AUTHOR, &puzzle.author) {
writeln!(dest, "{s}")?;
}
written = true;
}
if !is_blank(&puzzle.notes) {
writeln!(dest, "{}", puzzle.notes.trim_end())?;
written = true;
}
if written {
writeln!(dest)?;
}
for (idx, _) in puzzle.saved_games.iter().enumerate() {
self.write_saved_game(dest, puzzle, idx)?;
}
}
Ok(())
}
fn construct_tagged(tag: &str, data: &str) -> Option<String> {
(!is_blank(tag) && !is_blank(data)).then(|| format!("{}: {}", tag.trim(), data.trim()))
}
fn write_collection_header<W>(
&self, collection: &Collection, dest: &mut W,
) -> Result<(), IoWriteError>
where
W: Write,
{
if self.suppress_extra {
return Ok(());
}
for line in SOK_HEADER.lines() {
writeln!(dest, "{line}")?;
}
let mut some_written = false;
if let Some(s) = Self::construct_tagged(DATE_CREATED, &collection.created_at) {
writeln!(dest, "{s}")?;
some_written = true;
}
if let Some(s) = Self::construct_tagged(DATE_OF_LAST_CHANGE, &collection.updated_at) {
writeln!(dest, "{s}")?;
some_written = true;
}
if some_written {
writeln!(dest, "{}", RAW_FILE_NOTES.repeat(30))?;
}
some_written = false;
if let Some(s) = Self::construct_tagged(COLLECTION, &collection.title) {
writeln!(dest, "{s}")?;
some_written = true;
}
if let Some(s) = Self::construct_tagged(AUTHOR, &collection.author) {
writeln!(dest, "{s}")?;
some_written = true;
}
if !is_blank(&collection.notes) {
writeln!(dest, "{}", collection.notes.trim())?;
some_written = true;
}
if some_written {
writeln!(dest)?;
}
Ok(())
}
}
pub const SOK_HEADER: &str = r#####"::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
:: Sokoban (c) by Falcon Co., Ltd., Japan ::
::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
:: File Format 0.17 ::
::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
:: ::
:: File Notes Optional ::
:: Puzzle 1 Required ::
:: Title Optional* ::
:: Board See legend ::
:: Puzzle Notes Optional ::
:: Saved Game or Solution 1 Optional ::
:: Title Optional* ::
:: Moves See legend ::
:: Notes Optional ::
:: Saved Game or Solution 2 Optional ::
:: ... (more saved games and solutions) ::
:: Puzzle 2 Optional ::
:: ... (more puzzles) ::
:: ::
:: Remarks: ::
:: ::
:: File Notes ::
:: File notes consist of unstructured text and ::
:: key/value properties, such as "Author: Name". Lines ::
:: beginning with "::" are comments meant to be read ::
:: only by a person examining the file in a text ::
:: editor, and should not be displayed by the Sokoban ::
:: program. ::
:: ::
:: The optional but recommended property ::
:: "Collection: Name" assigns a name to the puzzle ::
:: collection. When a collection is copied from the ::
:: internet, for example, and pasted into a Sokoban ::
:: program, this information allows the collection to ::
:: be saved with the proper name. ::
:: ::
:: Titles ::
:: A title line is the last non-blank text line before ::
:: a board, a saved game, or a solution, provided the ::
:: line is preceded by a blank line or it is the only ::
:: text line at this position in the file. ::
:: ::
:: Title lines are optional unless a single or a last ::
:: text line from a preceding puzzle, saved game, ::
:: solution, or file header can be mistaken for a title ::
:: line. ::
:: ::
:: Puzzle Notes ::
:: Two special key/value pairs are supported in puzzle ::
:: notes: "Title" and "Author", hence, titles can ::
:: either come from a title line or from a key/value ::
:: pair. ::
:: ::
::::::::::::::::::::::::::: Board ::::::::::::::::::::::::::
:: Legend.................: :.................Legend ::
::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
:: Wall...................: # # :...................Wall ::
:: Pusher.................: p @ :.................Pusher ::
:: Pusher on goal square..: P + :..Pusher on goal square ::
:: Box....................: b $ :....................Box ::
:: Box on goal square.....: B * :.....Box on goal square ::
:: Goal square............: . . :............Goal square ::
:: Floor..................: :..................Floor ::
:: Floor..................: - _ :..................Floor ::
:: ::
:: Remarks: ::
:: ::
:: The first and the last non-empty square in each row ::
:: must be a wall or a box on a goal. An empty interior ::
:: row is written with at least one "-" or "_". ::
:: ::
:: Boards may be run-length encoded (RLE), e.g., ::
:: "###----p.#" may be encoded as "3#4-p.#", and ::
:: "#-#-#-##-#-#-#" may be encoded as "2(3(#-)#)". ::
:: A row cannot be split over multiple lines. ::
:: ::
:: Rows may be combined on a single line by using "|" ::
:: as a row separator, e.g., "--3#|3#-#|#pb.#|5#". ::
:: A "|" at the end of a line is optional and may be ::
:: omitted. ::
:: ::
::::::::::::::::::::::::::: Moves ::::::::::::::::::::::::::
:: Legend.................: :.................Legend ::
::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
:: Move pusher up.........: u U :.......Push/pull box up ::
:: Move pusher down.......: d D :.....Push/pull box down ::
:: Move pusher left.......: l L :.....Push/pull box left ::
:: Move pusher right......: r R :....Push/pull box right ::
:: Begin jump.............: [ ] :...............End jump ::
:: Begin pusher change....: { } :......End pusher change ::
:: Current position.......: * * :.......Current position ::
:: ::
:: Remarks: ::
:: ::
:: Moves may be run-length encoded, e.g., "3r4U" means ::
:: "rrrUUUU", and "2(3(dr)R)" means "drdrdrRdrdrdrR". ::
:: Each line must, however, have at least one proper ::
:: non-digit character. Spaces between moves are ::
:: allowed. ::
:: ::
:: Jumps and pulls: Only in reverse mode saved games ::
:: and solutions. ::
:: ::
:: Reverse mode saved games and solutions must begin ::
:: with a jump, even if it is empty. An example: ::
:: "[]U[rr]d". ::
:: ::
:: Pusher changes: Only in puzzles with multiple ::
:: pushers, e.g., Multiban. Moves inside the braces ::
:: depict the relative movement to get from the ::
:: currently active pusher to the next active pusher. ::
:: At game start, a "{...}" sequence activates the ::
:: pusher relative to the top-left pusher. An example: ::
:: "{rddd}Urr{uul}uLU". If the top-left pusher is the ::
:: first active pusher, then the empty "{}" can be ::
:: omitted. ::
:: ::
:: The current position is optional and defaults to the ::
:: position after the last move. ::
:: ::
::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::"#####;