really hacky .puz parser

This commit is contained in:
cjdenio committed 2026-08-31 21:07:14 -06:00
commit 7ed1840bab
4 files changed
+289

No files matched your search

+3
View File
@@ -0,0 +1,3 @@
module github.com/cjdenio/crossword
go 1.25.0
View File
Whitespace-only changes.
+96
View File
@@ -0,0 +1,96 @@
package main
import (
"encoding/json"
"fmt"
"log"
"os"
"strings"
"github.com/cjdenio/crossword/puz"
)
func RenderPuzzle(puzzle string, width, height int) string {
b := strings.Builder{}
b.WriteRune('┌')
for range (width * 2) + 1 {
b.WriteRune('─')
}
b.WriteString("┐\n")
for index, char := range puzzle {
if index%width == 0 {
b.WriteString("│ ")
}
switch char {
case '.':
b.WriteRune(0x2588)
case '-':
b.WriteRune('_')
default:
b.WriteRune(char)
}
if (index+1)%width == 0 {
b.WriteString(" │\n")
} else if char == '.' && puzzle[index+1] == '.' {
b.WriteRune(0x2588)
} else {
b.WriteRune(' ')
}
}
b.WriteRune('└')
for range (width * 2) + 1 {
b.WriteRune('─')
}
b.WriteString("┘\n")
return b.String()
}
func main() {
// fmt.Print("\x1b[?1049h")
// fmt.Print("\x1b[?25l")
// defer func() {
// fmt.Print("\x1b[?1049l")
// fmt.Print("\x1b[?25h")
// }()
filename := os.Args[1]
file, err := os.ReadFile(filename)
if err != nil {
log.Fatal(err)
}
puzzle, err := puz.ParsePuz(file)
if err != nil {
log.Fatal(err)
}
fmt.Printf("TITLE: %s\n", puzzle.Title)
fmt.Printf("AUTHOR: %s\n", puzzle.Author)
fmt.Println(RenderPuzzle(puzzle.State, puzzle.Width, puzzle.Height))
j, _ := json.Marshal(puzzle)
fmt.Println(string(j))
// for _, clue := range puzzle.Clues {
// if clue.Direction == puz.DirectionAcross {
// fmt.Printf(". %d-across: %s: %s\n", clue.Number, clue.Clue, clue.Solution)
// } else {
// fmt.Printf(". %d-down: %s: %s\n", clue.Number, clue.Clue, clue.Solution)
// }
// }
fmt.Printf("%s\n", puzzle.Copyright)
// for {
// n, _ := os.Stdin.Read(make([]byte, 1))
// if n > 0 {
// return
// }
// }
}
+190
View File
@@ -0,0 +1,190 @@
package puz
import (
"bufio"
"bytes"
"encoding/binary"
"errors"
"math"
"strings"
)
type Direction int
const (
DirectionAcross Direction = iota
DirectionDown
)
type Clue struct {
Clue string
Solution string
Cells []int
Direction Direction
Number int
}
type Puzzle struct {
ClueCount int
Width int
Height int
Title string
Author string
Copyright string
Solution string
State string
Clues []Clue
}
func startOfDownClue(puzzle string, width, index int) bool {
if puzzle[index] == '.' {
return false
}
if index < width {
return true
}
if puzzle[index-width] == '.' {
return true
}
return false
}
func startOfAcrossClue(puzzle string, width, index int) bool {
if puzzle[index] == '.' {
return false
}
if index%width == 0 {
return true
}
if puzzle[index-1] == '.' {
return true
}
return false
}
func ParsePuz(file []byte) (*Puzzle, error) {
// read magic bytes
magic := string(file[2:14])
if magic != "ACROSS&DOWN\x00" {
return nil, errors.New("not a valid .puz file")
}
puzzle := new(Puzzle)
// read header
puzzle.Width = int(file[0x2C])
puzzle.Height = int(file[0x2D])
puzzle.ClueCount = int(binary.LittleEndian.Uint16(file[0x2E:0x30]))
puzzleSize := puzzle.Width * puzzle.Height
solutionStart := 0x34
solutionEnd := 0x34 + puzzleSize
stateStart := solutionEnd
stateEnd := stateStart + puzzleSize
puzzle.Solution = string(file[solutionStart:solutionEnd])
puzzle.State = string(file[stateStart:stateEnd])
// read text
buf := bufio.NewReader(bytes.NewReader(file[stateEnd:]))
title, err := buf.ReadString(0x00)
if err != nil {
return nil, err
}
puzzle.Title = title[:len(title)-1]
author, err := buf.ReadString(0x00)
if err != nil {
return nil, err
}
puzzle.Author = author[:len(author)-1]
copyright, err := buf.ReadString(0x00)
if err != nil {
return nil, err
}
puzzle.Copyright = copyright[:len(copyright)-1]
// read clues
clues := make([]string, 0, puzzle.ClueCount)
for range puzzle.ClueCount {
clue, err := buf.ReadString(0x00)
if err != nil {
return nil, err
}
clues = append(clues, clue[:len(clue)-1])
}
// assign clue numbers
puzzle.Clues = make([]Clue, 0, puzzle.ClueCount)
clueNumber := 1
clueIndex := 0
for i, _ := range puzzle.Solution {
cellGetsNumber := false
if startOfAcrossClue(puzzle.Solution, puzzle.Width, i) {
cellGetsNumber = true
puzzle.Clues = append(puzzle.Clues, Clue{
Cells: []int{i},
Direction: DirectionAcross,
Number: clueNumber,
Clue: clues[clueIndex],
})
clueIndex++
}
if startOfDownClue(puzzle.Solution, puzzle.Width, i) {
cellGetsNumber = true
puzzle.Clues = append(puzzle.Clues, Clue{
Cells: []int{i},
Direction: DirectionDown,
Number: clueNumber,
Clue: clues[clueIndex],
})
clueIndex++
}
if cellGetsNumber {
clueNumber++
}
}
// get solutions
for i, clue := range puzzle.Clues {
var solution strings.Builder
currentCell := clue.Cells[0]
for {
solution.WriteByte(puzzle.Solution[currentCell])
if currentCell != clue.Cells[0] {
clue.Cells = append(clue.Cells, currentCell)
}
if clue.Direction == DirectionAcross {
if ((currentCell+1)%puzzle.Width == 0) || puzzle.Solution[currentCell+1] == '.' {
break
} else {
currentCell++
}
} else if clue.Direction == DirectionDown {
if int(math.Floor(float64(currentCell/puzzle.Width)))+1 == puzzle.Height || puzzle.Solution[currentCell+puzzle.Width] == '.' {
break
} else {
currentCell += puzzle.Width
}
}
}
puzzle.Clues[i].Solution = solution.String()
puzzle.Clues[i].Cells = clue.Cells
}
return puzzle, nil
}