mirror of
https://github.com/cjdenio/crossword.git
synced 2026-10-03 11:06:19 +00:00
support ipuz files
This commit is contained in:
7 files changed
+263
-52
No files matched your search
@@ -1,6 +1,6 @@
|
|||||||
all: bin/crossword bin/crossword-ssh
|
all: bin/crossword bin/crossword-ssh
|
||||||
|
|
||||||
bin/crossword: cmd/main.go puz/puz.go game/game.go
|
bin/crossword: cmd/main.go puz/*.go game/game.go
|
||||||
go build -o $@ ./cmd
|
go build -o $@ ./cmd
|
||||||
bin/crossword-ssh: ssh/main.go puz/puz.go game/game.go
|
bin/crossword-ssh: ssh/main.go puz/*.go game/game.go
|
||||||
go build -o $@ ./ssh
|
go build -o $@ ./ssh
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
Very silly terminal-based crossword solver. It can play .puz files from a variety of sources, including [Crosshare](https://crosshare.org).
|
Very silly terminal-based crossword solver. It can play .puz / .ipuz files from a variety of sources, including [Crosshare](https://crosshare.org).
|
||||||
|
|
||||||
- Requirements: [Go](https://go.dev)
|
- Requirements: [Go](https://go.dev)
|
||||||
- Build: `make`
|
- Build: `make`
|
||||||
@@ -16,5 +16,5 @@ todo
|
|||||||
- .puz checksums
|
- .puz checksums
|
||||||
- fix shift+enter / shift+tab in other terminals
|
- fix shift+enter / shift+tab in other terminals
|
||||||
- clear grid
|
- clear grid
|
||||||
- rebus (at least add message that rebus is not supported)
|
- rebus
|
||||||
- .ipuz files
|
- detect rebus for .puz files
|
||||||
+10
-2
@@ -2,6 +2,7 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bufio"
|
"bufio"
|
||||||
|
"encoding/json"
|
||||||
"flag"
|
"flag"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
@@ -20,10 +21,11 @@ func main() {
|
|||||||
}()
|
}()
|
||||||
|
|
||||||
debugMode := flag.Bool("debug", false, "")
|
debugMode := flag.Bool("debug", false, "")
|
||||||
|
dumpMode := flag.Bool("dump", false, "")
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
|
|
||||||
filename := flag.Arg(0)
|
filename := flag.Arg(0)
|
||||||
file, err := os.ReadFile(filename)
|
file, err := os.Open(filename)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
@@ -33,11 +35,17 @@ func main() {
|
|||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
puzzle, err := puz.ParsePuz(file)
|
puzzle, err := puz.LoadPuzzle(file)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if dumpMode != nil && *dumpMode {
|
||||||
|
json.NewEncoder(os.Stdout).Encode(puzzle)
|
||||||
|
fmt.Print("\r\n")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
fmt.Print("\x1b[?25l")
|
fmt.Print("\x1b[?25l")
|
||||||
|
|
||||||
defer func() {
|
defer func() {
|
||||||
|
|||||||
+29
-13
@@ -21,23 +21,25 @@ const (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type State struct {
|
type State struct {
|
||||||
Puzzle *puz.Puzzle
|
Puzzle *puz.Puzzle
|
||||||
PuzzleState []rune
|
PuzzleState []rune
|
||||||
SelectedCell int
|
SelectedCell int
|
||||||
SelectedClue *puz.Clue
|
SelectedClue *puz.Clue
|
||||||
LastKeySequence string
|
LastKeySequence string
|
||||||
DebugMode bool
|
DebugMode bool
|
||||||
SolveState SolveState
|
SolveState SolveState
|
||||||
CheckState []rune
|
CheckState []rune
|
||||||
|
RebusConfirmation bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewState(puzzle *puz.Puzzle) *State {
|
func NewState(puzzle *puz.Puzzle) *State {
|
||||||
return &State{
|
return &State{
|
||||||
Puzzle: puzzle,
|
Puzzle: puzzle,
|
||||||
PuzzleState: []rune(puzzle.State),
|
PuzzleState: []rune(puzzle.State),
|
||||||
SelectedClue: puzzle.Clues[0],
|
SelectedClue: puzzle.Clues[0],
|
||||||
SelectedCell: puzzle.Clues[0].Cells[0],
|
SelectedCell: puzzle.Clues[0].Cells[0],
|
||||||
CheckState: make([]rune, len(puzzle.State)),
|
CheckState: make([]rune, len(puzzle.State)),
|
||||||
|
RebusConfirmation: puzzle.HasRebus,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -242,6 +244,11 @@ func (state *State) CheckWord(clue *puz.Clue) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (state *State) RenderUI(w io.Writer) int {
|
func (state *State) RenderUI(w io.Writer) int {
|
||||||
|
if state.RebusConfirmation {
|
||||||
|
fmt.Fprint(w, "This puzzle contains a rebus. This tool does not support rebuses.\r\nPress 'y' to continue anyway, or press any other key to exit.\r\n")
|
||||||
|
return 2
|
||||||
|
}
|
||||||
|
|
||||||
uiHeight := 0
|
uiHeight := 0
|
||||||
|
|
||||||
fmt.Fprintf(w, "\r\nTITLE: %s\r\n", state.Puzzle.Title)
|
fmt.Fprintf(w, "\r\nTITLE: %s\r\n", state.Puzzle.Title)
|
||||||
@@ -424,6 +431,15 @@ func (state *State) HandleInput(buffer []byte) (exited bool) {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if state.RebusConfirmation {
|
||||||
|
if buffer[0] == 'y' {
|
||||||
|
state.RebusConfirmation = false
|
||||||
|
return false
|
||||||
|
} else {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
state.LastKeySequence = fmt.Sprintf("%v", buffer)
|
state.LastKeySequence = fmt.Sprintf("%v", buffer)
|
||||||
|
|
||||||
if buffer[0] == 3 {
|
if buffer[0] == 3 {
|
||||||
|
|||||||
+154
@@ -0,0 +1,154 @@
|
|||||||
|
package puz
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
type iPuzDimensions struct {
|
||||||
|
Width int `json:"width"`
|
||||||
|
Height int `json:"height"`
|
||||||
|
}
|
||||||
|
type iPuzClue struct {
|
||||||
|
Number int `json:"number"`
|
||||||
|
Clue string `json:"clue"`
|
||||||
|
}
|
||||||
|
type iPuzFile struct {
|
||||||
|
Title string `json:"title"`
|
||||||
|
Author string `json:"author"`
|
||||||
|
Copyright string `json:"copyright"`
|
||||||
|
Puzzle [][]any `json:"puzzle"`
|
||||||
|
Solution [][]string `json:"solution"`
|
||||||
|
Dimensions iPuzDimensions `json:"dimensions"`
|
||||||
|
Clues map[string][]iPuzClue `json:"clues"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func ParseIPuz(file []byte) (*Puzzle, error) {
|
||||||
|
var parsed iPuzFile
|
||||||
|
err := json.Unmarshal(file, &parsed)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
puzzle := new(Puzzle)
|
||||||
|
|
||||||
|
puzzle.Title = parsed.Title
|
||||||
|
puzzle.Author = parsed.Author
|
||||||
|
puzzle.Copyright = parsed.Copyright
|
||||||
|
|
||||||
|
puzzle.Width = parsed.Dimensions.Width
|
||||||
|
puzzle.Height = parsed.Dimensions.Height
|
||||||
|
|
||||||
|
puzzle.ClueCount = len(parsed.Clues["Across"]) + len(parsed.Clues["Down"])
|
||||||
|
|
||||||
|
clueToCellMap := make(map[int][2]int)
|
||||||
|
|
||||||
|
state := strings.Builder{}
|
||||||
|
for y, row := range parsed.Puzzle {
|
||||||
|
for x, cell := range row {
|
||||||
|
switch c := cell.(type) {
|
||||||
|
case string:
|
||||||
|
if c == "#" {
|
||||||
|
state.WriteRune('.')
|
||||||
|
} else {
|
||||||
|
if i, err := strconv.Atoi(c); err == nil {
|
||||||
|
clueToCellMap[i] = [2]int{x, y}
|
||||||
|
}
|
||||||
|
state.WriteRune('-')
|
||||||
|
}
|
||||||
|
case float64:
|
||||||
|
clueToCellMap[int(c)] = [2]int{x, y}
|
||||||
|
state.WriteRune('-')
|
||||||
|
default:
|
||||||
|
state.WriteRune('-')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
puzzle.State = state.String()
|
||||||
|
|
||||||
|
solution := strings.Builder{}
|
||||||
|
for _, row := range parsed.Solution {
|
||||||
|
for _, cell := range row {
|
||||||
|
if cell == "#" {
|
||||||
|
solution.WriteRune('.')
|
||||||
|
} else if len(cell) > 1 {
|
||||||
|
solution.WriteByte(cell[0])
|
||||||
|
puzzle.HasRebus = true
|
||||||
|
} else if len(cell) == 1 {
|
||||||
|
solution.WriteString(cell)
|
||||||
|
} else {
|
||||||
|
return nil, errors.New("invalid ipuz file")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
puzzle.Solution = solution.String()
|
||||||
|
|
||||||
|
puzzle.Clues = make([]*Clue, 0, puzzle.ClueCount)
|
||||||
|
puzzle.Cells = make([][2]*Clue, puzzle.Width*puzzle.Height)
|
||||||
|
|
||||||
|
for _, clue := range parsed.Clues["Across"] {
|
||||||
|
cellCoords := clueToCellMap[clue.Number]
|
||||||
|
|
||||||
|
clueCells := make([]int, 0)
|
||||||
|
clueSolution := strings.Builder{}
|
||||||
|
|
||||||
|
clueStruct := &Clue{
|
||||||
|
Clue: clue.Clue,
|
||||||
|
Direction: DirectionAcross,
|
||||||
|
Number: clue.Number,
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := cellCoords[0]; i < puzzle.Width; i++ {
|
||||||
|
cell := parsed.Solution[cellCoords[1]][i]
|
||||||
|
if cell == "#" {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
cellIdx := cellCoords[1]*puzzle.Width + i
|
||||||
|
|
||||||
|
clueCells = append(clueCells, cellIdx)
|
||||||
|
puzzle.Cells[cellIdx][0] = clueStruct
|
||||||
|
clueSolution.WriteByte(cell[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
clueStruct.Solution = clueSolution.String()
|
||||||
|
clueStruct.Cells = clueCells
|
||||||
|
|
||||||
|
puzzle.Clues = append(puzzle.Clues, clueStruct)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, clue := range parsed.Clues["Down"] {
|
||||||
|
cellCoords := clueToCellMap[clue.Number]
|
||||||
|
|
||||||
|
clueCells := make([]int, 0)
|
||||||
|
clueSolution := strings.Builder{}
|
||||||
|
|
||||||
|
clueStruct := &Clue{
|
||||||
|
Clue: clue.Clue,
|
||||||
|
Direction: DirectionDown,
|
||||||
|
Number: clue.Number,
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := cellCoords[1]; i < puzzle.Height; i++ {
|
||||||
|
cell := parsed.Solution[i][cellCoords[0]]
|
||||||
|
if cell == "#" {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
cellIdx := i*puzzle.Width + cellCoords[0]
|
||||||
|
|
||||||
|
clueCells = append(clueCells, cellIdx)
|
||||||
|
puzzle.Cells[cellIdx][1] = clueStruct
|
||||||
|
clueSolution.WriteByte(cell[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
clueStruct.Solution = clueSolution.String()
|
||||||
|
clueStruct.Cells = clueCells
|
||||||
|
|
||||||
|
puzzle.Clues = append(puzzle.Clues, clueStruct)
|
||||||
|
}
|
||||||
|
|
||||||
|
return puzzle, nil
|
||||||
|
}
|
||||||
-32
@@ -11,38 +11,6 @@ import (
|
|||||||
"golang.org/x/text/encoding/charmap"
|
"golang.org/x/text/encoding/charmap"
|
||||||
)
|
)
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
Cells [][2]*Clue
|
|
||||||
}
|
|
||||||
|
|
||||||
func startOfDownClue(puzzle string, width, height, index int) bool {
|
func startOfDownClue(puzzle string, width, height, index int) bool {
|
||||||
if puzzle[index] == '.' {
|
if puzzle[index] == '.' {
|
||||||
return false
|
return false
|
||||||
|
|||||||
@@ -0,0 +1,65 @@
|
|||||||
|
package puz
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"errors"
|
||||||
|
"io"
|
||||||
|
)
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
Cells [][2]*Clue
|
||||||
|
|
||||||
|
HasRebus bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func LoadPuzzle(r io.Reader) (*Puzzle, error) {
|
||||||
|
bufReader := bufio.NewReader(r)
|
||||||
|
header, err := bufReader.Peek(16)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if string(header[2:14]) == "ACROSS&DOWN\x00" {
|
||||||
|
file, err := io.ReadAll(bufReader)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return ParsePuz(file)
|
||||||
|
} else if header[0] == '{' {
|
||||||
|
file, err := io.ReadAll(bufReader)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return ParseIPuz(file)
|
||||||
|
} else {
|
||||||
|
return nil, errors.New("could not detect file type")
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in new issue
Block a user