mirror of
https://github.com/cjdenio/crossword.git
synced 2026-10-03 11:06:19 +00:00
Compare commits
17
Commits
6a83b1c263
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d50b572fb5 | ||
|
|
7b35f86e32 | ||
|
|
419f0ae3cf | ||
|
|
b918c5be23 | ||
|
|
60d1bd644c | ||
|
|
50be9ab605 | ||
|
|
b7c64dabe3 | ||
|
|
c139490dc2 | ||
|
|
21f9caec34 | ||
|
|
5d62a4b611 | ||
|
|
176c3997cd | ||
|
|
4f92cdd941 | ||
|
|
143020a21d | ||
|
|
644ce0d273 | ||
|
|
c1d024a97e | ||
|
|
0f1da4c461 | ||
|
|
e6798b1a67 |
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.
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
all: bin/crossword bin/crossword-ssh
|
all: bin/crossword bin/crossword-ssh
|
||||||
|
|
||||||
bin/crossword: main.go puz/puz.go game/game.go
|
bin/crossword: cmd/main.go puz/*.go game/game.go
|
||||||
go build -o $@ .
|
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,10 +1,16 @@
|
|||||||
|
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)
|
||||||
|
- Build: `make`
|
||||||
|
- Usage: `./bin/crossword <file>.puz`
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
todo
|
todo
|
||||||
|
|
||||||
- fix bug with that one 6x6 puzzle
|
|
||||||
- correctly calculate uiHeight with multi-line clues
|
|
||||||
- timer
|
- timer
|
||||||
- reveal grid/word
|
|
||||||
- .puz checksums
|
- .puz checksums
|
||||||
- fix shift+enter / short+tab in other terminals
|
- fix shift+enter / shift+tab in other terminals
|
||||||
- after entering letter, jump to next unfilled cell (even if it's backwards)
|
- rebus
|
||||||
- clear grid
|
|
||||||
+19
-12
@@ -2,6 +2,7 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bufio"
|
"bufio"
|
||||||
|
"encoding/json"
|
||||||
"flag"
|
"flag"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
@@ -12,6 +13,11 @@ import (
|
|||||||
"golang.org/x/term"
|
"golang.org/x/term"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func getTermWidth() int {
|
||||||
|
w, _, _ := term.GetSize(int(os.Stdin.Fd()))
|
||||||
|
return w
|
||||||
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
defer func() {
|
defer func() {
|
||||||
if r := recover(); r != nil {
|
if r := recover(); r != nil {
|
||||||
@@ -20,10 +26,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 +40,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() {
|
||||||
@@ -50,12 +63,7 @@ func main() {
|
|||||||
|
|
||||||
defer term.Restore(int(os.Stdin.Fd()), termState)
|
defer term.Restore(int(os.Stdin.Fd()), termState)
|
||||||
|
|
||||||
state := game.State{
|
state := game.NewState(puzzle)
|
||||||
Puzzle: puzzle,
|
|
||||||
PuzzleState: []rune(puzzle.State),
|
|
||||||
SelectedClue: puzzle.Clues[0],
|
|
||||||
SelectedCell: puzzle.Clues[0].Cells[0],
|
|
||||||
}
|
|
||||||
if debugMode != nil {
|
if debugMode != nil {
|
||||||
state.DebugMode = *debugMode
|
state.DebugMode = *debugMode
|
||||||
}
|
}
|
||||||
@@ -69,7 +77,7 @@ func main() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
uiHeight := state.RenderUI(os.Stdout)
|
uiHeight := state.RenderUI(os.Stdout, getTermWidth())
|
||||||
|
|
||||||
scanner := bufio.NewScanner(os.Stdin)
|
scanner := bufio.NewScanner(os.Stdin)
|
||||||
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) {
|
||||||
@@ -103,16 +111,15 @@ func main() {
|
|||||||
}
|
}
|
||||||
state.SelectedClue = nil
|
state.SelectedClue = nil
|
||||||
state.SelectedCell = -1
|
state.SelectedCell = -1
|
||||||
state.Goodbye = true
|
|
||||||
fmt.Printf("\r\x1b[%dA", uiHeight)
|
fmt.Printf("\r\x1b[%dA", uiHeight)
|
||||||
fmt.Print("\x1b[J")
|
fmt.Print("\x1b[J")
|
||||||
state.RenderUI(os.Stdout)
|
state.RenderUI(os.Stdout, getTermWidth())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Printf("\r\x1b[%dA", uiHeight)
|
fmt.Printf("\r\x1b[%dA", uiHeight)
|
||||||
fmt.Print("\x1b[J")
|
fmt.Print("\x1b[J")
|
||||||
|
|
||||||
uiHeight = state.RenderUI(os.Stdout)
|
uiHeight = state.RenderUI(os.Stdout, getTermWidth())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+274
-47
@@ -12,15 +12,90 @@ import (
|
|||||||
"github.com/cjdenio/crossword/puz"
|
"github.com/cjdenio/crossword/puz"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type SolveState int
|
||||||
|
|
||||||
|
const (
|
||||||
|
Unsolved SolveState = iota
|
||||||
|
FilledNotSolved
|
||||||
|
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
|
||||||
Goodbye bool
|
|
||||||
LastKeySequence string
|
LastKeySequence string
|
||||||
DebugMode bool
|
DebugMode bool
|
||||||
SolveState int
|
SolveState SolveState
|
||||||
|
CheckState []rune
|
||||||
|
UIState UIState
|
||||||
|
}
|
||||||
|
|
||||||
|
func RenderWithLineWrap(s string, width int) (out string, lines int) {
|
||||||
|
builder := strings.Builder{}
|
||||||
|
runes := []rune(s)
|
||||||
|
|
||||||
|
counter := 0
|
||||||
|
lines = 1
|
||||||
|
|
||||||
|
for i := 0; i < len(runes); i++ {
|
||||||
|
char := runes[i]
|
||||||
|
|
||||||
|
if char == '\n' {
|
||||||
|
builder.WriteString("\r\n")
|
||||||
|
lines++
|
||||||
|
counter = 0
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if char == '\r' && i < len(runes)-1 && runes[i+1] == '\n' {
|
||||||
|
builder.WriteString("\r\n")
|
||||||
|
lines++
|
||||||
|
counter = 0
|
||||||
|
i++
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
builder.WriteRune(char)
|
||||||
|
counter++
|
||||||
|
|
||||||
|
if counter >= width && i < len(runes)-1 {
|
||||||
|
lines++
|
||||||
|
builder.WriteString("\r\n")
|
||||||
|
counter = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
builder.WriteString("\r\n")
|
||||||
|
|
||||||
|
out = builder.String()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewState(puzzle *puz.Puzzle) *State {
|
||||||
|
state := StateGame
|
||||||
|
if puzzle.HasRebus {
|
||||||
|
state = StateRebusConfirmation
|
||||||
|
}
|
||||||
|
return &State{
|
||||||
|
Puzzle: puzzle,
|
||||||
|
PuzzleState: []rune(puzzle.State),
|
||||||
|
SelectedClue: puzzle.Clues[0],
|
||||||
|
SelectedCell: puzzle.Clues[0].Cells[0],
|
||||||
|
CheckState: make([]rune, len(puzzle.State)),
|
||||||
|
UIState: state,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (state *State) MoveCursor(direction int) {
|
func (state *State) MoveCursor(direction int) {
|
||||||
@@ -179,7 +254,7 @@ func (state *State) GridFilled() bool {
|
|||||||
return !slices.Contains(state.PuzzleState, '-')
|
return !slices.Contains(state.PuzzleState, '-')
|
||||||
}
|
}
|
||||||
|
|
||||||
func (state *State) CheckPuzzle() bool {
|
func (state *State) PuzzleSolved() bool {
|
||||||
for i, cell := range state.PuzzleState {
|
for i, cell := range state.PuzzleState {
|
||||||
if cell == '.' {
|
if cell == '.' {
|
||||||
continue
|
continue
|
||||||
@@ -191,10 +266,59 @@ func (state *State) CheckPuzzle() bool {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
func (state *State) RenderUI(w io.Writer) int {
|
func (state *State) CheckPuzzle() {
|
||||||
selectedClueCells := []int{}
|
result := make([]rune, len(state.PuzzleState))
|
||||||
if state.SelectedClue != nil {
|
|
||||||
selectedClueCells = state.SelectedClue.Cells
|
for i, cell := range state.PuzzleState {
|
||||||
|
if cell == '.' || cell == '-' {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if cell == rune(state.Puzzle.Solution[i]) {
|
||||||
|
result[i] = 'y'
|
||||||
|
} else {
|
||||||
|
result[i] = 'n'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
state.CheckState = result
|
||||||
|
}
|
||||||
|
|
||||||
|
func (state *State) CheckWord(clue *puz.Clue) {
|
||||||
|
for i, cellIdx := range clue.Cells {
|
||||||
|
cellState := state.PuzzleState[cellIdx]
|
||||||
|
if cellState == '-' || cellState == '.' {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if cellState == rune(clue.Solution[i]) {
|
||||||
|
state.CheckState[cellIdx] = 'y'
|
||||||
|
} else {
|
||||||
|
state.CheckState[cellIdx] = 'n'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {
|
||||||
|
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")
|
||||||
|
return 2
|
||||||
}
|
}
|
||||||
|
|
||||||
uiHeight := 0
|
uiHeight := 0
|
||||||
@@ -203,24 +327,45 @@ func (state *State) RenderUI(w io.Writer) 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
|
||||||
fmt.Fprint(w, RenderPuzzle(state.PuzzleState, state.Puzzle.Width, state.Puzzle.Height, selectedClueCells, state.SelectedCell)+"\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"
|
||||||
if state.SelectedClue.Direction == puz.DirectionAcross {
|
if state.SelectedClue.Direction == puz.DirectionAcross {
|
||||||
fmt.Fprintf(w, "%d-across: %s\r\n\r\n", state.SelectedClue.Number, state.SelectedClue.Clue)
|
directionStr = "across"
|
||||||
} else {
|
|
||||||
fmt.Fprintf(w, "%d-down: %s\r\n\r\n", state.SelectedClue.Number, state.SelectedClue.Clue)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
clueStr, clueLines := RenderWithLineWrap(fmt.Sprintf("%d-%s: %s", state.SelectedClue.Number, directionStr, state.SelectedClue.Clue), width)
|
||||||
|
w.Write([]byte(clueStr))
|
||||||
|
uiHeight += clueLines
|
||||||
|
io.WriteString(w, "\r\n")
|
||||||
|
uiHeight += 1
|
||||||
|
}
|
||||||
|
if state.Puzzle.Copyright != "" {
|
||||||
|
fmt.Fprintf(w, "%s\r\n\r\n", state.Puzzle.Copyright)
|
||||||
uiHeight += 2
|
uiHeight += 2
|
||||||
}
|
}
|
||||||
fmt.Fprintf(w, "%s\r\n", state.Puzzle.Copyright)
|
|
||||||
uiHeight += 1
|
switch state.UIState {
|
||||||
|
case StateGame:
|
||||||
|
fmt.Fprint(w, AnsiDimmed("[enter]/[tab]: next word | [ctrl+a]: menu | [ctrl+c]: exit\r\n"))
|
||||||
|
uiHeight += 1
|
||||||
|
case StateMenu:
|
||||||
|
fmt.Fprint(w, AnsiDimmed("[c]: check puzzle/word | [r]: reveal puzzle/word | [x]: clear grid | [q]: exit menu\r\n"))
|
||||||
|
uiHeight += 1
|
||||||
|
case StateCheckMenu:
|
||||||
|
fmt.Fprint(w, AnsiDimmed("[w]: check word | [p]: check puzzle | [q]: exit menu\r\n"))
|
||||||
|
uiHeight += 1
|
||||||
|
case StateRevealMenu:
|
||||||
|
fmt.Fprint(w, AnsiDimmed("[w]: reveal word | [p]: reveal puzzle | [q]: exit menu\r\n"))
|
||||||
|
uiHeight += 1
|
||||||
|
}
|
||||||
|
|
||||||
switch state.SolveState {
|
switch state.SolveState {
|
||||||
case 1:
|
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..."))
|
||||||
uiHeight += 2
|
uiHeight += 2
|
||||||
case 2:
|
case Solved:
|
||||||
fmt.Fprintf(w, "\r\n%s\r\n", AnsiGreen("The puzzle was solved!"))
|
fmt.Fprintf(w, "\r\n%s\r\n", AnsiGreen("The puzzle was solved!"))
|
||||||
uiHeight += 2
|
uiHeight += 2
|
||||||
}
|
}
|
||||||
@@ -230,11 +375,6 @@ func (state *State) RenderUI(w io.Writer) int {
|
|||||||
uiHeight += 2
|
uiHeight += 2
|
||||||
}
|
}
|
||||||
|
|
||||||
if state.Goodbye {
|
|
||||||
fmt.Fprint(w, "\r\nsee ya\r\n")
|
|
||||||
uiHeight += 2
|
|
||||||
}
|
|
||||||
|
|
||||||
return uiHeight
|
return uiHeight
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -260,17 +400,22 @@ func AnsiRed(s string) string {
|
|||||||
return fmt.Sprintf("\x1b[31m%s\x1b[0m", s)
|
return fmt.Sprintf("\x1b[31m%s\x1b[0m", s)
|
||||||
}
|
}
|
||||||
|
|
||||||
func RenderPuzzle(puzzle []rune, width, height int, selectedClueCells []int, selectedCell int) string {
|
func (state *State) RenderPuzzle() string {
|
||||||
|
selectedClueCells := []int{}
|
||||||
|
if state.SelectedClue != nil {
|
||||||
|
selectedClueCells = state.SelectedClue.Cells
|
||||||
|
}
|
||||||
|
|
||||||
b := strings.Builder{}
|
b := strings.Builder{}
|
||||||
|
|
||||||
b.WriteRune('┌')
|
b.WriteRune('┌')
|
||||||
for range (width * 2) + 1 {
|
for range (state.Puzzle.Width * 2) + 1 {
|
||||||
b.WriteRune('─')
|
b.WriteRune('─')
|
||||||
}
|
}
|
||||||
b.WriteString("┐\r\n")
|
b.WriteString("┐\r\n")
|
||||||
|
|
||||||
for index, char := range puzzle {
|
for index, char := range state.PuzzleState {
|
||||||
if index%width == 0 {
|
if index%state.Puzzle.Width == 0 {
|
||||||
b.WriteString("│ ")
|
b.WriteString("│ ")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -284,17 +429,26 @@ func RenderPuzzle(puzzle []rune, width, height int, selectedClueCells []int, sel
|
|||||||
cell = string(char)
|
cell = string(char)
|
||||||
}
|
}
|
||||||
|
|
||||||
if selectedCell == index {
|
if state.SelectedCell == index {
|
||||||
cell = AnsiInverted(cell)
|
cell = AnsiInverted(cell)
|
||||||
} else if slices.Contains(selectedClueCells, index) {
|
} else if slices.Contains(selectedClueCells, index) {
|
||||||
cell = AnsiWhiteBackgrounded(cell)
|
cell = AnsiWhiteBackgrounded(cell)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if len(state.CheckState) == len(state.PuzzleState) {
|
||||||
|
switch state.CheckState[index] {
|
||||||
|
case 'y':
|
||||||
|
cell = AnsiGreen(cell)
|
||||||
|
case 'n':
|
||||||
|
cell = AnsiRed(cell)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
b.WriteString(cell)
|
b.WriteString(cell)
|
||||||
|
|
||||||
if (index+1)%width == 0 {
|
if (index+1)%state.Puzzle.Width == 0 {
|
||||||
b.WriteString(" │\r\n")
|
b.WriteString(" │\r\n")
|
||||||
} else if char == '.' && puzzle[index+1] == '.' {
|
} else if char == '.' && state.PuzzleState[index+1] == '.' {
|
||||||
b.WriteRune(0x2588)
|
b.WriteRune(0x2588)
|
||||||
} else {
|
} else {
|
||||||
// inefficient
|
// inefficient
|
||||||
@@ -307,7 +461,7 @@ func RenderPuzzle(puzzle []rune, width, height int, selectedClueCells []int, sel
|
|||||||
}
|
}
|
||||||
|
|
||||||
b.WriteRune('└')
|
b.WriteRune('└')
|
||||||
for range (width * 2) + 1 {
|
for range (state.Puzzle.Width * 2) + 1 {
|
||||||
b.WriteRune('─')
|
b.WriteRune('─')
|
||||||
}
|
}
|
||||||
b.WriteString("┘\r\n")
|
b.WriteString("┘\r\n")
|
||||||
@@ -348,6 +502,32 @@ func (state *State) LoadSaveFile(f []byte) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (state *State) SwitchDirections() bool {
|
||||||
|
switch state.SelectedClue.Direction {
|
||||||
|
case puz.DirectionAcross:
|
||||||
|
if state.Puzzle.Cells[state.SelectedCell][1] != nil {
|
||||||
|
state.SelectedClue = state.Puzzle.Cells[state.SelectedCell][1]
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
case puz.DirectionDown:
|
||||||
|
if state.Puzzle.Cells[state.SelectedCell][0] != nil {
|
||||||
|
state.SelectedClue = state.Puzzle.Cells[state.SelectedCell][0]
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
@@ -359,28 +539,67 @@ func (state *State) HandleInput(buffer []byte) (exited bool) {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
if buffer[0] == ' ' {
|
switch state.UIState {
|
||||||
switch state.SelectedClue.Direction {
|
case StateRebusConfirmation:
|
||||||
case puz.DirectionAcross:
|
if buffer[0] == 'y' {
|
||||||
if state.Puzzle.Cells[state.SelectedCell][1] != nil {
|
state.UIState = StateGame
|
||||||
state.SelectedClue = state.Puzzle.Cells[state.SelectedCell][1]
|
return false
|
||||||
}
|
} else {
|
||||||
case puz.DirectionDown:
|
return true
|
||||||
if state.Puzzle.Cells[state.SelectedCell][0] != nil {
|
|
||||||
state.SelectedClue = state.Puzzle.Cells[state.SelectedCell][0]
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
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] == ' ' {
|
||||||
|
state.SwitchDirections()
|
||||||
}
|
}
|
||||||
|
|
||||||
if buffer[0] >= 0x61 && buffer[0] <= 0x7a {
|
if buffer[0] >= 0x61 && buffer[0] <= 0x7a {
|
||||||
cellWasFilled := state.PuzzleState[state.SelectedCell] != '-'
|
cellWasFilled := state.PuzzleState[state.SelectedCell] != '-'
|
||||||
state.PuzzleState[state.SelectedCell] = unicode.ToUpper(rune(buffer[0]))
|
state.PuzzleState[state.SelectedCell] = unicode.ToUpper(rune(buffer[0]))
|
||||||
|
state.CheckState[state.SelectedCell] = 0x00
|
||||||
|
|
||||||
i := slices.Index(state.SelectedClue.Cells, state.SelectedCell)
|
i := slices.Index(state.SelectedClue.Cells, state.SelectedCell)
|
||||||
|
|
||||||
if !cellWasFilled {
|
if !cellWasFilled {
|
||||||
// jump to next unfilled cell in clue
|
// jump to next unfilled cell in clue
|
||||||
for x := i + 1; x < len(state.SelectedClue.Cells); x++ {
|
for x := (i + 1) % len(state.SelectedClue.Cells); x != i; x = ((x + 1) % len(state.SelectedClue.Cells)) {
|
||||||
if state.PuzzleState[state.SelectedClue.Cells[x]] == '-' {
|
if state.PuzzleState[state.SelectedClue.Cells[x]] == '-' {
|
||||||
state.SelectedCell = state.SelectedClue.Cells[x]
|
state.SelectedCell = state.SelectedClue.Cells[x]
|
||||||
break
|
break
|
||||||
@@ -394,14 +613,14 @@ func (state *State) HandleInput(buffer []byte) (exited bool) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if state.GridFilled() {
|
if state.GridFilled() {
|
||||||
if state.CheckPuzzle() {
|
if state.PuzzleSolved() {
|
||||||
state.SolveState = 2
|
state.SolveState = Solved
|
||||||
return true
|
return true
|
||||||
} else {
|
} else {
|
||||||
state.SolveState = 1
|
state.SolveState = FilledNotSolved
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
state.SolveState = 0
|
state.SolveState = Unsolved
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -427,9 +646,9 @@ func (state *State) HandleInput(buffer []byte) (exited bool) {
|
|||||||
|
|
||||||
if string(buffer[0:2]) == "\x1b[" {
|
if string(buffer[0:2]) == "\x1b[" {
|
||||||
if (buffer[2] == 68 || buffer[2] == 67) && state.SelectedClue != nil && state.SelectedClue.Direction == puz.DirectionDown {
|
if (buffer[2] == 68 || buffer[2] == 67) && state.SelectedClue != nil && state.SelectedClue.Direction == puz.DirectionDown {
|
||||||
state.SelectedClue = state.Puzzle.Cells[state.SelectedCell][0]
|
state.SwitchDirections()
|
||||||
} else if (buffer[2] == 65 || buffer[2] == 66) && state.SelectedClue != nil && state.SelectedClue.Direction == puz.DirectionAcross {
|
} else if (buffer[2] == 65 || buffer[2] == 66) && state.SelectedClue != nil && state.SelectedClue.Direction == puz.DirectionAcross {
|
||||||
state.SelectedClue = state.Puzzle.Cells[state.SelectedCell][1]
|
state.SwitchDirections()
|
||||||
} else {
|
} else {
|
||||||
switch buffer[2] {
|
switch buffer[2] {
|
||||||
case 68: // left
|
case 68: // left
|
||||||
@@ -444,9 +663,17 @@ func (state *State) HandleInput(buffer []byte) (exited bool) {
|
|||||||
|
|
||||||
switch state.SelectedClue.Direction {
|
switch state.SelectedClue.Direction {
|
||||||
case puz.DirectionAcross:
|
case puz.DirectionAcross:
|
||||||
state.SelectedClue = state.Puzzle.Cells[state.SelectedCell][0]
|
if state.Puzzle.Cells[state.SelectedCell][0] != nil {
|
||||||
|
state.SelectedClue = state.Puzzle.Cells[state.SelectedCell][0]
|
||||||
|
} else {
|
||||||
|
state.SelectedClue = state.Puzzle.Cells[state.SelectedCell][1]
|
||||||
|
}
|
||||||
case puz.DirectionDown:
|
case puz.DirectionDown:
|
||||||
state.SelectedClue = state.Puzzle.Cells[state.SelectedCell][1]
|
if state.Puzzle.Cells[state.SelectedCell][1] != nil {
|
||||||
|
state.SelectedClue = state.Puzzle.Cells[state.SelectedCell][1]
|
||||||
|
} else {
|
||||||
|
state.SelectedClue = state.Puzzle.Cells[state.SelectedCell][0]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+183
@@ -0,0 +1,183 @@
|
|||||||
|
package puz
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"slices"
|
||||||
|
"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 {
|
||||||
|
Kind []string `json:"kind"`
|
||||||
|
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 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) {
|
||||||
|
var parsed iPuzFile
|
||||||
|
err := json.Unmarshal(file, &parsed)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(parsed.Kind) == 0 || !slices.ContainsFunc(parsed.Kind, func(k string) bool {
|
||||||
|
return strings.HasPrefix(k, "http://ipuz.org/crossword")
|
||||||
|
}) {
|
||||||
|
return nil, errors.New("ipuz file is not a crossword")
|
||||||
|
}
|
||||||
|
|
||||||
|
puzzle := new(Puzzle)
|
||||||
|
|
||||||
|
puzzle.RebusCells = make(map[int]string)
|
||||||
|
|
||||||
|
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 {
|
||||||
|
unwrappedCell, err := unwrapCell(cell)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
switch c := unwrappedCell.(type) {
|
||||||
|
case string:
|
||||||
|
state.WriteRune('.')
|
||||||
|
case int:
|
||||||
|
clueToCellMap[c] = [2]int{x, y}
|
||||||
|
state.WriteRune('-')
|
||||||
|
default:
|
||||||
|
return nil, errors.New("unexpected type in .ipuz file")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
puzzle.State = state.String()
|
||||||
|
|
||||||
|
solution := strings.Builder{}
|
||||||
|
for y, row := range parsed.Solution {
|
||||||
|
for x, cell := range row {
|
||||||
|
if cell == "#" {
|
||||||
|
solution.WriteRune('.')
|
||||||
|
} else if len(cell) > 1 {
|
||||||
|
solution.WriteByte(cell[0])
|
||||||
|
puzzle.RebusCells[y*puzzle.Width+x] = cell
|
||||||
|
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
|
||||||
|
}
|
||||||
+166
-53
@@ -5,44 +5,15 @@ import (
|
|||||||
"bytes"
|
"bytes"
|
||||||
"encoding/binary"
|
"encoding/binary"
|
||||||
"errors"
|
"errors"
|
||||||
|
"io"
|
||||||
"math"
|
"math"
|
||||||
|
"regexp"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"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
|
||||||
@@ -75,60 +46,160 @@ func startOfAcrossClue(puzzle string, width, index int) bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
func readText(b *bufio.Reader) (string, error) {
|
func readText(b *bufio.Reader, version string) (string, error) {
|
||||||
text, err := b.ReadString(0x00)
|
text, err := b.ReadString(0x00)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
decoded, err := charmap.ISO8859_1.NewDecoder().String(text[:len(text)-1])
|
if version == "1" {
|
||||||
|
decoded, err := charmap.ISO8859_1.NewDecoder().String(text[:len(text)-1])
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return strings.TrimSpace(decoded), nil
|
||||||
|
} else {
|
||||||
|
return strings.TrimSpace(string(text[:len(text)-1])), nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {
|
if err != nil {
|
||||||
return "", err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return decoded, nil
|
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)
|
||||||
|
|
||||||
|
puzzle.RebusCells = make(map[int]string)
|
||||||
|
|
||||||
|
_, err = r.Seek(10, io.SeekCurrent)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
// read header
|
// read header
|
||||||
puzzle.Width = int(file[0x2C])
|
version := make([]byte, 4)
|
||||||
puzzle.Height = int(file[0x2D])
|
_, err = r.Read(version)
|
||||||
puzzle.ClueCount = int(binary.LittleEndian.Uint16(file[0x2E:0x30]))
|
if err != nil {
|
||||||
|
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)
|
||||||
|
|
||||||
title, err := readText(buf)
|
// read text fields
|
||||||
|
buf := bufio.NewReader(r)
|
||||||
|
|
||||||
|
title, err := readText(buf, string(version[0]))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
puzzle.Title = title
|
puzzle.Title = title
|
||||||
|
|
||||||
author, err := readText(buf)
|
author, err := readText(buf, string(version[0]))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
puzzle.Author = author
|
puzzle.Author = author
|
||||||
|
|
||||||
copyright, err := readText(buf)
|
copyright, err := readText(buf, string(version[0]))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -137,13 +208,55 @@ func ParsePuz(file []byte) (*Puzzle, error) {
|
|||||||
// read clues
|
// read clues
|
||||||
clues := make([]string, 0, puzzle.ClueCount)
|
clues := make([]string, 0, puzzle.ClueCount)
|
||||||
for range puzzle.ClueCount {
|
for range puzzle.ClueCount {
|
||||||
clue, err := readText(buf)
|
clue, err := readText(buf, string(version[0]))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
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
|
||||||
|
}
|
||||||
|
|
||||||
|
extraBlocks := make(map[string]*ExtraBlock)
|
||||||
|
|
||||||
|
for {
|
||||||
|
extra, err := readExtraBlock(buf)
|
||||||
|
if errors.Is(err, io.EOF) {
|
||||||
|
break
|
||||||
|
} else if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
extraBlocks[extra.Name] = extra
|
||||||
|
}
|
||||||
|
|
||||||
|
grbs, grbsOk := extraBlocks["GRBS"]
|
||||||
|
trbl, trblOk := extraBlocks["RTBL"]
|
||||||
|
if grbsOk && trblOk {
|
||||||
|
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]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 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))
|
||||||
|
|||||||
@@ -0,0 +1,67 @@
|
|||||||
|
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
|
||||||
|
|
||||||
|
RebusCells map[int]string
|
||||||
|
}
|
||||||
|
|
||||||
|
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")
|
||||||
|
}
|
||||||
|
}
|
||||||
+34
-16
@@ -13,22 +13,25 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
file, err := os.ReadFile(os.Args[1])
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
port := ":2222"
|
||||||
|
if portEnv, ok := os.LookupEnv("PORT"); ok && portEnv != "" {
|
||||||
|
port = ":" + portEnv
|
||||||
|
}
|
||||||
|
|
||||||
ssh.Handle(func(s ssh.Session) {
|
ssh.Handle(func(s ssh.Session) {
|
||||||
file, err := os.ReadFile(os.Args[1])
|
log.Printf("new connection from %s (username %s)\n", s.RemoteAddr().String(), s.User())
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
puzzle, err := puz.ParsePuz(file)
|
puzzle, err := puz.ParsePuz(file)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
state := game.State{
|
state := game.NewState(puzzle)
|
||||||
Puzzle: puzzle,
|
|
||||||
PuzzleState: []rune(puzzle.State),
|
|
||||||
SelectedClue: puzzle.Clues[0],
|
|
||||||
SelectedCell: puzzle.Clues[0].Cells[0],
|
|
||||||
}
|
|
||||||
|
|
||||||
io.WriteString(s, "\x1b[?25l")
|
io.WriteString(s, "\x1b[?25l")
|
||||||
|
|
||||||
@@ -36,7 +39,20 @@ func main() {
|
|||||||
io.WriteString(s, "\x1b[?25h")
|
io.WriteString(s, "\x1b[?25h")
|
||||||
}()
|
}()
|
||||||
|
|
||||||
uiHeight := state.RenderUI(s)
|
pty, winCh, isPty := s.Pty()
|
||||||
|
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) {
|
||||||
@@ -66,18 +82,20 @@ func main() {
|
|||||||
if exited {
|
if exited {
|
||||||
state.SelectedClue = nil
|
state.SelectedClue = nil
|
||||||
state.SelectedCell = -1
|
state.SelectedCell = -1
|
||||||
state.Goodbye = true
|
|
||||||
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)
|
state.RenderUI(s, windowWidth)
|
||||||
return
|
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)
|
uiHeight = state.RenderUI(s, windowWidth)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
log.Printf("%s disconnected\n", s.RemoteAddr().String())
|
||||||
})
|
})
|
||||||
log.Fatal(ssh.ListenAndServe(":1234", nil))
|
log.Printf("starting on port %s...\n", port)
|
||||||
|
log.Fatal(ssh.ListenAndServe(port, nil))
|
||||||
}
|
}
|
||||||
Reference in new issue
Block a user