mirror of
https://github.com/cjdenio/crossword.git
synced 2026-10-03 19:06:19 +00:00
Compare commits
7
Commits
60d1bd644c
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1f208f80b7 | ||
|
|
19e295493e | ||
|
|
6242535184 | ||
|
|
d50b572fb5 | ||
|
|
7b35f86e32 | ||
|
|
419f0ae3cf | ||
|
|
b918c5be23 |
No files matched your search
@@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2026 Caleb Denio
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
@@ -2,7 +2,7 @@ Very silly terminal-based crossword solver. It can play .puz / .ipuz files from
|
|||||||
|
|
||||||
- Requirements: [Go](https://go.dev)
|
- Requirements: [Go](https://go.dev)
|
||||||
- Build: `make`
|
- Build: `make`
|
||||||
- Usage: `./bin/crossword <file>.puz`
|
- Usage: `./bin/crossword <file>.{ipuz,puz}`
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
|
|||||||
+34
-10
@@ -40,6 +40,7 @@ type State struct {
|
|||||||
SolveState SolveState
|
SolveState SolveState
|
||||||
CheckState []rune
|
CheckState []rune
|
||||||
UIState UIState
|
UIState UIState
|
||||||
|
NoteVisible bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func RenderWithLineWrap(s string, width int) (out string, lines int) {
|
func RenderWithLineWrap(s string, width int) (out string, lines int) {
|
||||||
@@ -327,8 +328,17 @@ func (state *State) RenderUI(w io.Writer, width int) int {
|
|||||||
uiHeight += 2
|
uiHeight += 2
|
||||||
fmt.Fprintf(w, "AUTHOR: %s\r\n", state.Puzzle.Author)
|
fmt.Fprintf(w, "AUTHOR: %s\r\n", state.Puzzle.Author)
|
||||||
uiHeight += 1
|
uiHeight += 1
|
||||||
|
|
||||||
|
if state.Puzzle.Note != "" && state.NoteVisible {
|
||||||
|
note, noteLines := RenderWithLineWrap(state.Puzzle.Note, min(width, 60))
|
||||||
|
|
||||||
|
fmt.Fprintf(w, "\r\n%s\r\n", AnsiDimmed(note))
|
||||||
|
uiHeight += noteLines + 2
|
||||||
|
}
|
||||||
|
|
||||||
fmt.Fprint(w, state.RenderPuzzle()+"\r\n")
|
fmt.Fprint(w, state.RenderPuzzle()+"\r\n")
|
||||||
uiHeight += state.Puzzle.Height + 3
|
uiHeight += state.Puzzle.Height + 3
|
||||||
|
|
||||||
if state.SelectedClue != nil {
|
if state.SelectedClue != nil {
|
||||||
directionStr := "down"
|
directionStr := "down"
|
||||||
if state.SelectedClue.Direction == puz.DirectionAcross {
|
if state.SelectedClue.Direction == puz.DirectionAcross {
|
||||||
@@ -341,22 +351,29 @@ func (state *State) RenderUI(w io.Writer, width int) int {
|
|||||||
io.WriteString(w, "\r\n")
|
io.WriteString(w, "\r\n")
|
||||||
uiHeight += 1
|
uiHeight += 1
|
||||||
}
|
}
|
||||||
fmt.Fprintf(w, "%s\r\n", state.Puzzle.Copyright)
|
if state.Puzzle.Copyright != "" {
|
||||||
uiHeight += 1
|
fmt.Fprintf(w, "%s\r\n\r\n", state.Puzzle.Copyright)
|
||||||
|
uiHeight += 2
|
||||||
|
}
|
||||||
|
|
||||||
switch state.UIState {
|
switch state.UIState {
|
||||||
case StateGame:
|
case StateGame:
|
||||||
fmt.Fprintln(w, AnsiDimmed("\r\n[enter]/[tab]: next word | [ctrl+a]: menu | [ctrl+c]: exit"))
|
line := "[enter]/[tab]: next word | [ctrl+a]: menu"
|
||||||
uiHeight += 2
|
if state.Puzzle.Note != "" {
|
||||||
|
line += " | [ctrl+n]: toggle note"
|
||||||
|
}
|
||||||
|
line += " | [ctrl+c]: exit\r\n"
|
||||||
|
fmt.Fprint(w, AnsiDimmed(line))
|
||||||
|
uiHeight += 1
|
||||||
case StateMenu:
|
case StateMenu:
|
||||||
fmt.Fprintln(w, AnsiDimmed("\r\n[c]: check puzzle/word | [r]: reveal puzzle/word | [x]: clear grid | [q]: exit menu"))
|
fmt.Fprint(w, AnsiDimmed("[c]: check puzzle/word | [r]: reveal puzzle/word | [x]: clear grid | [q]: exit menu\r\n"))
|
||||||
uiHeight += 2
|
uiHeight += 1
|
||||||
case StateCheckMenu:
|
case StateCheckMenu:
|
||||||
fmt.Fprintln(w, AnsiDimmed("\r\n[w]: check word | [p]: check puzzle | [q]: exit menu"))
|
fmt.Fprint(w, AnsiDimmed("[w]: check word | [p]: check puzzle | [q]: exit menu\r\n"))
|
||||||
uiHeight += 2
|
uiHeight += 1
|
||||||
case StateRevealMenu:
|
case StateRevealMenu:
|
||||||
fmt.Fprintln(w, AnsiDimmed("\r\n[w]: reveal word | [p]: reveal puzzle | [q]: exit menu"))
|
fmt.Fprint(w, AnsiDimmed("[w]: reveal word | [p]: reveal puzzle | [q]: exit menu\r\n"))
|
||||||
uiHeight += 2
|
uiHeight += 1
|
||||||
}
|
}
|
||||||
|
|
||||||
switch state.SolveState {
|
switch state.SolveState {
|
||||||
@@ -584,6 +601,11 @@ func (state *State) HandleInput(buffer []byte) (exited bool) {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if buffer[0] == '\x0E' {
|
||||||
|
state.NoteVisible = !state.NoteVisible
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
if buffer[0] == ' ' {
|
if buffer[0] == ' ' {
|
||||||
state.SwitchDirections()
|
state.SwitchDirections()
|
||||||
}
|
}
|
||||||
@@ -626,10 +648,12 @@ func (state *State) HandleInput(buffer []byte) (exited bool) {
|
|||||||
// is there a filled cell underneath the cursor?
|
// is there a filled cell underneath the cursor?
|
||||||
if state.PuzzleState[state.SelectedCell] != '-' {
|
if state.PuzzleState[state.SelectedCell] != '-' {
|
||||||
state.PuzzleState[state.SelectedCell] = '-'
|
state.PuzzleState[state.SelectedCell] = '-'
|
||||||
|
state.CheckState[state.SelectedCell] = 0x00
|
||||||
} else {
|
} else {
|
||||||
i := slices.Index(state.SelectedClue.Cells, state.SelectedCell)
|
i := slices.Index(state.SelectedClue.Cells, state.SelectedCell)
|
||||||
if i > 0 {
|
if i > 0 {
|
||||||
state.PuzzleState[state.SelectedClue.Cells[i-1]] = '-' // clear the previous cell
|
state.PuzzleState[state.SelectedClue.Cells[i-1]] = '-' // clear the previous cell
|
||||||
|
state.CheckState[state.SelectedClue.Cells[i-1]] = 0x00
|
||||||
state.SelectedCell = state.SelectedClue.Cells[i-1]
|
state.SelectedCell = state.SelectedClue.Cells[i-1]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+7
-2
@@ -21,6 +21,7 @@ type iPuzFile struct {
|
|||||||
Title string `json:"title"`
|
Title string `json:"title"`
|
||||||
Author string `json:"author"`
|
Author string `json:"author"`
|
||||||
Copyright string `json:"copyright"`
|
Copyright string `json:"copyright"`
|
||||||
|
Notes string `json:"notes"`
|
||||||
Puzzle [][]any `json:"puzzle"`
|
Puzzle [][]any `json:"puzzle"`
|
||||||
Solution [][]string `json:"solution"`
|
Solution [][]string `json:"solution"`
|
||||||
Dimensions iPuzDimensions `json:"dimensions"`
|
Dimensions iPuzDimensions `json:"dimensions"`
|
||||||
@@ -63,9 +64,12 @@ func ParseIPuz(file []byte) (*Puzzle, error) {
|
|||||||
|
|
||||||
puzzle := new(Puzzle)
|
puzzle := new(Puzzle)
|
||||||
|
|
||||||
|
puzzle.RebusCells = make(map[int]string)
|
||||||
|
|
||||||
puzzle.Title = parsed.Title
|
puzzle.Title = parsed.Title
|
||||||
puzzle.Author = parsed.Author
|
puzzle.Author = parsed.Author
|
||||||
puzzle.Copyright = parsed.Copyright
|
puzzle.Copyright = parsed.Copyright
|
||||||
|
puzzle.Note = parsed.Notes
|
||||||
|
|
||||||
puzzle.Width = parsed.Dimensions.Width
|
puzzle.Width = parsed.Dimensions.Width
|
||||||
puzzle.Height = parsed.Dimensions.Height
|
puzzle.Height = parsed.Dimensions.Height
|
||||||
@@ -95,12 +99,13 @@ func ParseIPuz(file []byte) (*Puzzle, error) {
|
|||||||
puzzle.State = state.String()
|
puzzle.State = state.String()
|
||||||
|
|
||||||
solution := strings.Builder{}
|
solution := strings.Builder{}
|
||||||
for _, row := range parsed.Solution {
|
for y, row := range parsed.Solution {
|
||||||
for _, cell := range row {
|
for x, cell := range row {
|
||||||
if cell == "#" {
|
if cell == "#" {
|
||||||
solution.WriteRune('.')
|
solution.WriteRune('.')
|
||||||
} else if len(cell) > 1 {
|
} else if len(cell) > 1 {
|
||||||
solution.WriteByte(cell[0])
|
solution.WriteByte(cell[0])
|
||||||
|
puzzle.RebusCells[y*puzzle.Width+x] = cell
|
||||||
puzzle.HasRebus = true
|
puzzle.HasRebus = true
|
||||||
} else if len(cell) == 1 {
|
} else if len(cell) == 1 {
|
||||||
solution.WriteString(cell)
|
solution.WriteString(cell)
|
||||||
|
|||||||
+31
-3
@@ -7,6 +7,8 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"io"
|
"io"
|
||||||
"math"
|
"math"
|
||||||
|
"regexp"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"golang.org/x/text/encoding/charmap"
|
"golang.org/x/text/encoding/charmap"
|
||||||
@@ -123,6 +125,8 @@ func ParsePuz(file []byte) (*Puzzle, error) {
|
|||||||
|
|
||||||
puzzle := new(Puzzle)
|
puzzle := new(Puzzle)
|
||||||
|
|
||||||
|
puzzle.RebusCells = make(map[int]string)
|
||||||
|
|
||||||
_, err = r.Seek(10, io.SeekCurrent)
|
_, err = r.Seek(10, io.SeekCurrent)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -211,11 +215,13 @@ func ParsePuz(file []byte) (*Puzzle, error) {
|
|||||||
clues = append(clues, clue)
|
clues = append(clues, clue)
|
||||||
}
|
}
|
||||||
|
|
||||||
// read the "note" field, currently unused
|
note, err := readText(buf, string(version[0]))
|
||||||
_, err = readText(buf, string(version[0]))
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
puzzle.Note = note
|
||||||
|
|
||||||
|
extraBlocks := make(map[string]*ExtraBlock)
|
||||||
|
|
||||||
for {
|
for {
|
||||||
extra, err := readExtraBlock(buf)
|
extra, err := readExtraBlock(buf)
|
||||||
@@ -224,8 +230,30 @@ func ParsePuz(file []byte) (*Puzzle, error) {
|
|||||||
} else if err != nil {
|
} else if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if extra.Name == "RTBL" {
|
extraBlocks[extra.Name] = extra
|
||||||
|
}
|
||||||
|
|
||||||
|
grbs, grbsOk := extraBlocks["GRBS"]
|
||||||
|
trbl, trblOk := extraBlocks["RTBL"]
|
||||||
|
if grbsOk && trblOk {
|
||||||
puzzle.HasRebus = true
|
puzzle.HasRebus = true
|
||||||
|
rebusCellMap := make(map[int][]int)
|
||||||
|
for i, b := range grbs.Data {
|
||||||
|
if b > 0 {
|
||||||
|
rebusCellMap[int(b)-1] = append(rebusCellMap[int(b)-1], i)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
rebusData := regexp.MustCompile(`\s*(\d+):(\w+);\s*`).FindAllStringSubmatch(string(trbl.Data), -1)
|
||||||
|
for _, rebus := range rebusData {
|
||||||
|
cell, err := strconv.Atoi(rebus[1])
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, c := range rebusCellMap[cell] {
|
||||||
|
puzzle.RebusCells[c] = rebus[2]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ type Puzzle struct {
|
|||||||
Title string
|
Title string
|
||||||
Author string
|
Author string
|
||||||
Copyright string
|
Copyright string
|
||||||
|
Note string
|
||||||
|
|
||||||
Solution string
|
Solution string
|
||||||
State string
|
State string
|
||||||
@@ -38,6 +39,8 @@ type Puzzle struct {
|
|||||||
Cells [][2]*Clue
|
Cells [][2]*Clue
|
||||||
|
|
||||||
HasRebus bool
|
HasRebus bool
|
||||||
|
|
||||||
|
RebusCells map[int]string
|
||||||
}
|
}
|
||||||
|
|
||||||
func LoadPuzzle(r io.Reader) (*Puzzle, error) {
|
func LoadPuzzle(r io.Reader) (*Puzzle, error) {
|
||||||
|
|||||||
+16
-4
@@ -39,8 +39,20 @@ func main() {
|
|||||||
io.WriteString(s, "\x1b[?25h")
|
io.WriteString(s, "\x1b[?25h")
|
||||||
}()
|
}()
|
||||||
|
|
||||||
pty, _, _ := s.Pty() // TODO: actually handle terminal size changes
|
pty, winCh, isPty := s.Pty()
|
||||||
uiHeight := state.RenderUI(s, pty.Window.Width)
|
if !isPty {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
windowWidth := pty.Window.Width
|
||||||
|
|
||||||
|
uiHeight := state.RenderUI(s, windowWidth)
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
for window := range winCh {
|
||||||
|
windowWidth = window.Width
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
scanner := bufio.NewScanner(s)
|
scanner := bufio.NewScanner(s)
|
||||||
scanner.Split(func(data []byte, atEOF bool) (advance int, token []byte, err error) {
|
scanner.Split(func(data []byte, atEOF bool) (advance int, token []byte, err error) {
|
||||||
@@ -72,14 +84,14 @@ func main() {
|
|||||||
state.SelectedCell = -1
|
state.SelectedCell = -1
|
||||||
fmt.Fprintf(s, "\r\x1b[%dA", uiHeight)
|
fmt.Fprintf(s, "\r\x1b[%dA", uiHeight)
|
||||||
fmt.Fprint(s, "\x1b[J")
|
fmt.Fprint(s, "\x1b[J")
|
||||||
state.RenderUI(s, pty.Window.Width)
|
state.RenderUI(s, windowWidth)
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Fprintf(s, "\r\x1b[%dA", uiHeight)
|
fmt.Fprintf(s, "\r\x1b[%dA", uiHeight)
|
||||||
fmt.Fprint(s, "\x1b[J")
|
fmt.Fprint(s, "\x1b[J")
|
||||||
|
|
||||||
uiHeight = state.RenderUI(s, pty.Window.Width)
|
uiHeight = state.RenderUI(s, windowWidth)
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Printf("%s disconnected\n", s.RemoteAddr().String())
|
log.Printf("%s disconnected\n", s.RemoteAddr().String())
|
||||||
|
|||||||
Reference in new issue
Block a user