mirror of
https://github.com/cjdenio/crossword.git
synced 2026-10-03 11:06:19 +00:00
refactor .puz parser + detect rebuses
This commit is contained in:
2 files changed
+126
-15
No files matched your search
@@ -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
@@ -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))
|
||||||
|
|||||||
Reference in new issue
Block a user