mirror of
https://github.com/cjdenio/crossword.git
synced 2026-10-03 11:06:19 +00:00
Compare commits
3
Commits
c139490dc2
...
60d1bd644c
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
60d1bd644c | ||
|
|
50be9ab605 | ||
|
|
b7c64dabe3 |
No files matched your search
@@ -11,9 +11,6 @@ Very silly terminal-based crossword solver. It can play .puz / .ipuz files from
|
|||||||
todo
|
todo
|
||||||
|
|
||||||
- timer
|
- timer
|
||||||
- reveal grid/word
|
|
||||||
- .puz checksums
|
- .puz checksums
|
||||||
- fix shift+enter / shift+tab in other terminals
|
- fix shift+enter / shift+tab in other terminals
|
||||||
- clear grid
|
|
||||||
- rebus
|
- rebus
|
||||||
- detect rebus for .puz files
|
|
||||||
+118
-25
@@ -20,16 +20,26 @@ const (
|
|||||||
Solved
|
Solved
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type UIState int
|
||||||
|
|
||||||
|
const (
|
||||||
|
StateGame UIState = iota
|
||||||
|
StateRebusConfirmation
|
||||||
|
StateMenu
|
||||||
|
StateRevealMenu
|
||||||
|
StateCheckMenu
|
||||||
|
)
|
||||||
|
|
||||||
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
|
UIState UIState
|
||||||
}
|
}
|
||||||
|
|
||||||
func RenderWithLineWrap(s string, width int) (out string, lines int) {
|
func RenderWithLineWrap(s string, width int) (out string, lines int) {
|
||||||
@@ -74,13 +84,17 @@ func RenderWithLineWrap(s string, width int) (out string, lines int) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func NewState(puzzle *puz.Puzzle) *State {
|
func NewState(puzzle *puz.Puzzle) *State {
|
||||||
|
state := StateGame
|
||||||
|
if puzzle.HasRebus {
|
||||||
|
state = StateRebusConfirmation
|
||||||
|
}
|
||||||
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,
|
UIState: state,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -284,8 +298,25 @@ func (state *State) CheckWord(clue *puz.Clue) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (state *State) RevealWord(clue *puz.Clue) {
|
||||||
|
for i, cellIdx := range clue.Cells {
|
||||||
|
state.PuzzleState[cellIdx] = rune(clue.Solution[i])
|
||||||
|
state.CheckState[cellIdx] = 'y'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (state *State) RevealPuzzle() {
|
||||||
|
for i, cell := range state.PuzzleState {
|
||||||
|
if cell == '.' {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
state.PuzzleState[i] = rune(state.Puzzle.Solution[i])
|
||||||
|
state.CheckState[i] = 'y'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (state *State) RenderUI(w io.Writer, width int) int {
|
func (state *State) RenderUI(w io.Writer, width int) int {
|
||||||
if state.RebusConfirmation {
|
if state.UIState == StateRebusConfirmation {
|
||||||
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")
|
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
|
return 2
|
||||||
}
|
}
|
||||||
@@ -313,6 +344,21 @@ func (state *State) RenderUI(w io.Writer, width int) int {
|
|||||||
fmt.Fprintf(w, "%s\r\n", state.Puzzle.Copyright)
|
fmt.Fprintf(w, "%s\r\n", state.Puzzle.Copyright)
|
||||||
uiHeight += 1
|
uiHeight += 1
|
||||||
|
|
||||||
|
switch state.UIState {
|
||||||
|
case StateGame:
|
||||||
|
fmt.Fprintln(w, AnsiDimmed("\r\n[enter]/[tab]: next word | [ctrl+a]: menu | [ctrl+c]: exit"))
|
||||||
|
uiHeight += 2
|
||||||
|
case StateMenu:
|
||||||
|
fmt.Fprintln(w, AnsiDimmed("\r\n[c]: check puzzle/word | [r]: reveal puzzle/word | [x]: clear grid | [q]: exit menu"))
|
||||||
|
uiHeight += 2
|
||||||
|
case StateCheckMenu:
|
||||||
|
fmt.Fprintln(w, AnsiDimmed("\r\n[w]: check word | [p]: check puzzle | [q]: exit menu"))
|
||||||
|
uiHeight += 2
|
||||||
|
case StateRevealMenu:
|
||||||
|
fmt.Fprintln(w, AnsiDimmed("\r\n[w]: reveal word | [p]: reveal puzzle | [q]: exit menu"))
|
||||||
|
uiHeight += 2
|
||||||
|
}
|
||||||
|
|
||||||
switch state.SolveState {
|
switch state.SolveState {
|
||||||
case FilledNotSolved:
|
case FilledNotSolved:
|
||||||
fmt.Fprintf(w, "\r\n%s\r\n", AnsiRed("The puzzle was filled, but at least 1 letter is incorrect..."))
|
fmt.Fprintf(w, "\r\n%s\r\n", AnsiRed("The puzzle was filled, but at least 1 letter is incorrect..."))
|
||||||
@@ -471,26 +517,73 @@ func (state *State) SwitchDirections() bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (state *State) ClearGrid() {
|
||||||
|
for i, cell := range state.PuzzleState {
|
||||||
|
if cell != '.' {
|
||||||
|
state.PuzzleState[i] = '-'
|
||||||
|
state.CheckState[i] = '\x00'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (state *State) HandleInput(buffer []byte) (exited bool) {
|
func (state *State) HandleInput(buffer []byte) (exited bool) {
|
||||||
if len(buffer) == 0 {
|
if len(buffer) == 0 {
|
||||||
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 {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
switch state.UIState {
|
||||||
|
case StateRebusConfirmation:
|
||||||
|
if buffer[0] == 'y' {
|
||||||
|
state.UIState = StateGame
|
||||||
|
return false
|
||||||
|
} else {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
case StateMenu:
|
||||||
|
switch buffer[0] {
|
||||||
|
case 'c':
|
||||||
|
state.UIState = StateCheckMenu
|
||||||
|
case 'r':
|
||||||
|
state.UIState = StateRevealMenu
|
||||||
|
case 'x':
|
||||||
|
state.ClearGrid()
|
||||||
|
state.UIState = StateGame
|
||||||
|
default:
|
||||||
|
state.UIState = StateGame
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
case StateCheckMenu:
|
||||||
|
switch buffer[0] {
|
||||||
|
case 'w':
|
||||||
|
state.CheckWord(state.SelectedClue)
|
||||||
|
case 'p':
|
||||||
|
state.CheckPuzzle()
|
||||||
|
}
|
||||||
|
state.UIState = StateGame
|
||||||
|
return false
|
||||||
|
case StateRevealMenu:
|
||||||
|
switch buffer[0] {
|
||||||
|
case 'w':
|
||||||
|
state.RevealWord(state.SelectedClue)
|
||||||
|
case 'p':
|
||||||
|
state.RevealPuzzle()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
state.UIState = StateGame
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if buffer[0] == '\x01' {
|
||||||
|
state.UIState = StateMenu
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
if buffer[0] == ' ' {
|
if buffer[0] == ' ' {
|
||||||
state.SwitchDirections()
|
state.SwitchDirections()
|
||||||
}
|
}
|
||||||
|
|||||||
+30
-12
@@ -27,6 +27,27 @@ type iPuzFile struct {
|
|||||||
Clues map[string][]iPuzClue `json:"clues"`
|
Clues map[string][]iPuzClue `json:"clues"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func unwrapCell(cell any) (any, error) {
|
||||||
|
switch c := cell.(type) {
|
||||||
|
case string:
|
||||||
|
if c == "#" {
|
||||||
|
return "#", nil
|
||||||
|
} else if i, err := strconv.Atoi(c); err == nil {
|
||||||
|
return i, nil
|
||||||
|
} else {
|
||||||
|
return nil, errors.New("invalid string")
|
||||||
|
}
|
||||||
|
case float64:
|
||||||
|
return int(c), nil
|
||||||
|
case map[string]any:
|
||||||
|
if unwrappedCell, ok := c["cell"]; ok {
|
||||||
|
return unwrapCell(unwrappedCell) // recursion babyyyyyy
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, errors.New("unknown type")
|
||||||
|
}
|
||||||
|
|
||||||
func ParseIPuz(file []byte) (*Puzzle, error) {
|
func ParseIPuz(file []byte) (*Puzzle, error) {
|
||||||
var parsed iPuzFile
|
var parsed iPuzFile
|
||||||
err := json.Unmarshal(file, &parsed)
|
err := json.Unmarshal(file, &parsed)
|
||||||
@@ -56,21 +77,18 @@ func ParseIPuz(file []byte) (*Puzzle, error) {
|
|||||||
state := strings.Builder{}
|
state := strings.Builder{}
|
||||||
for y, row := range parsed.Puzzle {
|
for y, row := range parsed.Puzzle {
|
||||||
for x, cell := range row {
|
for x, cell := range row {
|
||||||
switch c := cell.(type) {
|
unwrappedCell, err := unwrapCell(cell)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
switch c := unwrappedCell.(type) {
|
||||||
case string:
|
case string:
|
||||||
if c == "#" {
|
state.WriteRune('.')
|
||||||
state.WriteRune('.')
|
case int:
|
||||||
} else {
|
clueToCellMap[c] = [2]int{x, y}
|
||||||
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('-')
|
state.WriteRune('-')
|
||||||
default:
|
default:
|
||||||
state.WriteRune('-')
|
return nil, errors.New("unexpected type in .ipuz file")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+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