refactor .puz parser + detect rebuses

This commit is contained in:
cjdenio committed 2026-09-29 21:20:13 -04:00
1 parent b7c64dabe3
commit 50be9ab605
2 files changed
+126 -15

No files matched your search

-1
View File
@@ -16,4 +16,3 @@ todo
- fix shift+enter / shift+tab in other terminals - fix shift+enter / shift+tab in other terminals
- clear grid - clear grid
- rebus - rebus
- detect rebus for .puz files
+126 -14
View File
@@ -5,6 +5,7 @@ import (
"bytes" "bytes"
"encoding/binary" "encoding/binary"
"errors" "errors"
"io"
"math" "math"
"strings" "strings"
@@ -61,33 +62,126 @@ func readText(b *bufio.Reader, version string) (string, error) {
} }
} }
type ExtraBlock struct {
Name string
Data []byte
}
func readExtraBlock(r *bufio.Reader) (*ExtraBlock, error) {
name := make([]byte, 4)
_, err := r.Read(name)
if err != nil {
return nil, err
}
length := make([]byte, 2)
_, err = r.Read(length)
if err != nil {
return nil, err
}
_, err = r.Discard(2)
if err != nil {
return nil, err
}
data := make([]byte, binary.LittleEndian.Uint16(length))
_, err = r.Read(data)
if err != nil {
return nil, err
}
// consume trailing null byte
_, err = r.Discard(1)
if err != nil {
return nil, err
}
return &ExtraBlock{
Name: string(name),
Data: data,
}, nil
}
func ParsePuz(file []byte) (*Puzzle, error) { func ParsePuz(file []byte) (*Puzzle, error) {
r := bytes.NewReader(file)
_, err := r.Seek(2, io.SeekStart) // consume the checksum
if err != nil {
return nil, err
}
// read magic bytes // read magic bytes
magic := string(file[2:14]) magic := make([]byte, 12)
if magic != "ACROSS&DOWN\x00" { _, err = r.Read(magic)
if err != nil {
return nil, err
}
if !bytes.Equal(magic, []byte("ACROSS&DOWN\x00")) {
return nil, errors.New("not a valid .puz file") return nil, errors.New("not a valid .puz file")
} }
puzzle := new(Puzzle) puzzle := new(Puzzle)
_, err = r.Seek(10, io.SeekCurrent)
if err != nil {
return nil, err
}
// read header // read header
version := string(file[0x18:0x1B]) version := make([]byte, 4)
puzzle.Width = int(file[0x2C]) _, err = r.Read(version)
puzzle.Height = int(file[0x2D]) if err != nil {
puzzle.ClueCount = int(binary.LittleEndian.Uint16(file[0x2E:0x30])) return nil, err
}
_, err = r.Seek(16, io.SeekCurrent)
if err != nil {
return nil, err
}
width, err := r.ReadByte()
if err != nil {
return nil, err
}
height, err := r.ReadByte()
if err != nil {
return nil, err
}
puzzle.Width = int(width)
puzzle.Height = int(height)
clueCount := make([]byte, 2)
_, err = r.Read(clueCount)
if err != nil {
return nil, err
}
puzzle.ClueCount = int(binary.LittleEndian.Uint16(clueCount))
puzzleSize := puzzle.Width * puzzle.Height puzzleSize := puzzle.Width * puzzle.Height
solutionStart := 0x34
solutionEnd := 0x34 + puzzleSize
stateStart := solutionEnd _, err = r.Seek(4, io.SeekCurrent)
stateEnd := stateStart + puzzleSize if err != nil {
return nil, err
}
puzzle.Solution = string(file[solutionStart:solutionEnd]) solution := make([]byte, puzzleSize)
puzzle.State = string(file[stateStart:stateEnd]) _, err = r.Read(solution)
if err != nil {
return nil, err
}
state := make([]byte, puzzleSize)
_, err = r.Read(state)
if err != nil {
return nil, err
}
// read text puzzle.Solution = string(solution)
buf := bufio.NewReader(bytes.NewReader(file[stateEnd:])) puzzle.State = string(state)
// read text fields
buf := bufio.NewReader(r)
title, err := readText(buf, string(version[0])) title, err := readText(buf, string(version[0]))
if err != nil { if err != nil {
@@ -117,6 +211,24 @@ func ParsePuz(file []byte) (*Puzzle, error) {
clues = append(clues, clue) clues = append(clues, clue)
} }
// read the "note" field, currently unused
_, err = readText(buf, string(version[0]))
if err != nil {
return nil, err
}
for {
extra, err := readExtraBlock(buf)
if errors.Is(err, io.EOF) {
break
} else if err != nil {
return nil, err
}
if extra.Name == "RTBL" {
puzzle.HasRebus = true
}
}
// assign clue numbers // assign clue numbers
puzzle.Clues = make([]*Clue, 0, puzzle.ClueCount) puzzle.Clues = make([]*Clue, 0, puzzle.ClueCount)
puzzle.Cells = make([][2]*Clue, len(puzzle.Solution)) puzzle.Cells = make([][2]*Clue, len(puzzle.Solution))