commit 7ed1840bab241b6b31d81260ad13338dd6e289fe Author: Caleb Denio Date: Mon Aug 31 21:07:14 2026 -0600 really hacky .puz parser diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..3340cb9 --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module github.com/cjdenio/crossword + +go 1.25.0 diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..e69de29 diff --git a/main.go b/main.go new file mode 100644 index 0000000..27cbf3a --- /dev/null +++ b/main.go @@ -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 + // } + // } +} diff --git a/puz/puz.go b/puz/puz.go new file mode 100644 index 0000000..348a839 --- /dev/null +++ b/puz/puz.go @@ -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 +}