many improvements

This commit is contained in:
cjdenio committed 2026-09-03 22:05:22 -06:00
1 parent b7d1d34273
commit 9e9d81ca16
3 files changed
+311 -247

No files matched your search

+1 -1
View File
@@ -5,4 +5,4 @@ todo
- check puzzle when grid is filled - check puzzle when grid is filled
- fix bug with that one 6x6 puzzle - fix bug with that one 6x6 puzzle
- correctly calculate uiHeight with multi-line clues - correctly calculate uiHeight with multi-line clues
- handle multiple keypresses in one `read` - timer
+244
View File
@@ -0,0 +1,244 @@
package main
import (
"fmt"
"slices"
"strings"
"github.com/cjdenio/crossword/puz"
)
type State struct {
Puzzle *puz.Puzzle
PuzzleState []rune
SelectedCell int
SelectedClue *puz.Clue
Goodbye bool
LastKeySequence string
DebugMode bool
SolveState int
}
func (state *State) MoveCursor(direction int) {
switch direction {
case 0: // up
if state.SelectedCell < state.Puzzle.Width { // if already in the top row, do nothing
return
}
for i := state.SelectedCell - state.Puzzle.Width; i >= 0; i -= state.Puzzle.Width {
if state.PuzzleState[i] != '.' {
state.SelectedCell = i
return
}
}
case 1: // right
if (state.SelectedCell+1)%state.Puzzle.Width == 0 { // if already in the right column, do nothing
return
}
for i := state.SelectedCell + 1; i%state.Puzzle.Width != 0; i += 1 {
if state.PuzzleState[i] != '.' {
state.SelectedCell = i
return
}
}
case 2: // down
if (state.SelectedCell) >= (state.Puzzle.Width*state.Puzzle.Height)-state.Puzzle.Width { // if already in the bottom row, do nothing
return
}
for i := state.SelectedCell + state.Puzzle.Width; i < (state.Puzzle.Width * state.Puzzle.Height); i += state.Puzzle.Width {
if state.PuzzleState[i] != '.' {
state.SelectedCell = i
return
}
}
case 3: // left
if state.SelectedCell%state.Puzzle.Width == 0 { // if already in the left column, do nothing
return
}
for i := state.SelectedCell - 1; (i+1)%state.Puzzle.Width != 0; i -= 1 {
if state.PuzzleState[i] != '.' {
state.SelectedCell = i
return
}
}
}
}
func (state *State) FirstUnfilledCellForClue(clue *puz.Clue) int {
for _, cell := range clue.Cells {
if state.PuzzleState[cell] == '-' {
return cell
}
}
return clue.Cells[0]
}
func (state *State) NextWord() {
if state.SelectedClue == nil {
return
}
foundSelectedClue := false
for _, clue := range state.Puzzle.Clues {
if !foundSelectedClue && clue == state.SelectedClue {
foundSelectedClue = true
continue
}
if foundSelectedClue && clue.Direction == state.SelectedClue.Direction {
state.SelectedClue = clue
state.SelectedCell = state.FirstUnfilledCellForClue(clue)
return
}
}
for _, clue := range state.Puzzle.Clues {
if clue.Direction != state.SelectedClue.Direction {
state.SelectedClue = clue
state.SelectedCell = state.FirstUnfilledCellForClue(clue)
return
}
}
}
func (state *State) GridFilled() bool {
return !slices.Contains(state.PuzzleState, '-')
}
func (state *State) CheckPuzzle() bool {
for i, cell := range state.PuzzleState {
if cell == '.' {
continue
}
if cell != rune(state.Puzzle.Solution[i]) {
return false
}
}
return true
}
func (state *State) RenderUI() int {
selectedClueCells := []int{}
if state.SelectedClue != nil {
selectedClueCells = state.SelectedClue.Cells
}
uiHeight := 0
fmt.Printf("\r\nTITLE: %s\r\n", state.Puzzle.Title)
uiHeight += 2
fmt.Printf("AUTHOR: %s\r\n", state.Puzzle.Author)
uiHeight += 1
fmt.Print(RenderPuzzle(state.PuzzleState, state.Puzzle.Width, state.Puzzle.Height, selectedClueCells, state.SelectedCell) + "\r\n")
uiHeight += state.Puzzle.Height + 3
if state.SelectedClue != nil {
if state.SelectedClue.Direction == puz.DirectionAcross {
fmt.Printf("%d-across: %s\r\n\r\n", state.SelectedClue.Number, state.SelectedClue.Clue)
} else {
fmt.Printf("%d-down: %s\r\n\r\n", state.SelectedClue.Number, state.SelectedClue.Clue)
}
uiHeight += 2
}
fmt.Printf("%s\r\n", state.Puzzle.Copyright)
uiHeight += 1
switch state.SolveState {
case 1:
fmt.Print("\r\nThe puzzle was filled, but at least 1 letter is incorrect...\r\n")
uiHeight += 2
case 2:
fmt.Printf("\r\n%s\r\n", AnsiGreen("The puzzle was solved!"))
uiHeight += 2
}
if state.LastKeySequence != "" && state.DebugMode {
fmt.Printf("\r\n%s\r\n", AnsiDimmed(state.LastKeySequence))
uiHeight += 2
}
if state.Goodbye {
fmt.Print("\r\nsee ya\r\n")
uiHeight += 2
}
return uiHeight
}
const (
AnsiInvert string = "\x1b[7m"
AnsiReset string = "\x1b[m"
AnsiWhiteBackground string = "\x1b[100m"
)
func AnsiInverted(s string) string {
return AnsiInvert + s + AnsiReset
}
func AnsiWhiteBackgrounded(s string) string {
return AnsiWhiteBackground + s + AnsiReset
}
func AnsiDimmed(s string) string {
return fmt.Sprintf("\x1b[2m%s\x1b[0m", s)
}
func AnsiGreen(s string) string {
return fmt.Sprintf("\x1b[32m%s\x1b[0m", s)
}
func RenderPuzzle(puzzle []rune, width, height int, selectedClueCells []int, selectedCell int) string {
b := strings.Builder{}
b.WriteRune('┌')
for range (width * 2) + 1 {
b.WriteRune('─')
}
b.WriteString("┐\r\n")
for index, char := range puzzle {
if index%width == 0 {
b.WriteString("│ ")
}
cell := ""
switch char {
case '.':
cell = string(rune(0x2588))
case '-':
cell = AnsiDimmed("_")
default:
cell = string(char)
}
if selectedCell == index {
cell = AnsiInverted(cell)
} else if slices.Contains(selectedClueCells, index) {
cell = AnsiWhiteBackgrounded(cell)
}
b.WriteString(cell)
if (index+1)%width == 0 {
b.WriteString(" │\r\n")
} else if char == '.' && puzzle[index+1] == '.' {
b.WriteRune(0x2588)
} else {
// inefficient
if slices.Contains(selectedClueCells, index) && slices.Contains(selectedClueCells, index+1) {
b.WriteString(AnsiWhiteBackgrounded(" "))
} else {
b.WriteRune(' ')
}
}
}
b.WriteRune('└')
for range (width * 2) + 1 {
b.WriteRune('─')
}
b.WriteString("┘\r\n")
return b.String()
}
+66 -246
View File
@@ -1,237 +1,19 @@
package main package main
import ( import (
"bufio"
"flag" "flag"
"fmt" "fmt"
"log" "log"
"os" "os"
"slices" "slices"
"strings"
"unicode" "unicode"
"github.com/cjdenio/crossword/puz" "github.com/cjdenio/crossword/puz"
"golang.org/x/term" "golang.org/x/term"
) )
const (
AnsiInvert string = "\x1b[7m"
AnsiReset string = "\x1b[m"
AnsiWhiteBackground string = "\x1b[100m"
)
func AnsiInverted(s string) string {
return AnsiInvert + s + AnsiReset
}
func AnsiWhiteBackgrounded(s string) string {
return AnsiWhiteBackground + s + AnsiReset
}
func AnsiDimmed(s string) string {
return fmt.Sprintf("\x1b[2m%s\x1b[0m", s)
}
func RenderPuzzle(puzzle []rune, width, height int, selectedClueCells []int, selectedCell int) string {
b := strings.Builder{}
b.WriteRune('┌')
for range (width * 2) + 1 {
b.WriteRune('─')
}
b.WriteString("┐\r\n")
for index, char := range puzzle {
if index%width == 0 {
b.WriteString("│ ")
}
cell := ""
switch char {
case '.':
cell = string(rune(0x2588))
case '-':
cell = AnsiDimmed("_")
default:
cell = string(char)
}
if selectedCell == index {
cell = AnsiInverted(cell)
} else if slices.Contains(selectedClueCells, index) {
cell = AnsiWhiteBackgrounded(cell)
}
b.WriteString(cell)
if (index+1)%width == 0 {
b.WriteString(" │\r\n")
} else if char == '.' && puzzle[index+1] == '.' {
b.WriteRune(0x2588)
} else {
// inefficient
if slices.Contains(selectedClueCells, index) && slices.Contains(selectedClueCells, index+1) {
b.WriteString(AnsiWhiteBackgrounded(" "))
} else {
b.WriteRune(' ')
}
}
}
b.WriteRune('└')
for range (width * 2) + 1 {
b.WriteRune('─')
}
b.WriteString("┘\r\n")
return b.String()
}
type State struct {
Puzzle *puz.Puzzle
PuzzleState []rune
SelectedCell int
SelectedClue *puz.Clue
Goodbye bool
LastKeySequence string
DebugMode bool
}
func (state *State) MoveCursor(direction int) {
switch direction {
case 0: // up
if state.SelectedCell < state.Puzzle.Width { // if already in the top row, do nothing
return
}
for i := state.SelectedCell - state.Puzzle.Width; i >= 0; i -= state.Puzzle.Width {
if state.PuzzleState[i] != '.' {
state.SelectedCell = i
return
}
}
case 1: // right
if (state.SelectedCell+1)%state.Puzzle.Width == 0 { // if already in the right column, do nothing
return
}
for i := state.SelectedCell + 1; i%state.Puzzle.Width != 0; i += 1 {
if state.PuzzleState[i] != '.' {
state.SelectedCell = i
return
}
}
case 2: // down
if (state.SelectedCell) >= (state.Puzzle.Width*state.Puzzle.Height)-state.Puzzle.Width { // if already in the bottom row, do nothing
return
}
for i := state.SelectedCell + state.Puzzle.Width; i < (state.Puzzle.Width * state.Puzzle.Height); i += state.Puzzle.Width {
if state.PuzzleState[i] != '.' {
state.SelectedCell = i
return
}
}
case 3: // left
if state.SelectedCell%state.Puzzle.Width == 0 { // if already in the left column, do nothing
return
}
for i := state.SelectedCell - 1; (i+1)%state.Puzzle.Width != 0; i -= 1 {
if state.PuzzleState[i] != '.' {
state.SelectedCell = i
return
}
}
}
}
func (state *State) FirstUnfilledCellForClue(clue *puz.Clue) int {
for _, cell := range clue.Cells {
if state.PuzzleState[cell] == '-' {
return cell
}
}
return clue.Cells[0]
}
func (state *State) NextWord() {
if state.SelectedClue == nil {
return
}
foundSelectedClue := false
for _, clue := range state.Puzzle.Clues {
if !foundSelectedClue && clue == state.SelectedClue {
foundSelectedClue = true
continue
}
if foundSelectedClue && clue.Direction == state.SelectedClue.Direction {
state.SelectedClue = clue
state.SelectedCell = state.FirstUnfilledCellForClue(clue)
return
}
}
for _, clue := range state.Puzzle.Clues {
if clue.Direction != state.SelectedClue.Direction {
state.SelectedClue = clue
state.SelectedCell = state.FirstUnfilledCellForClue(clue)
return
}
}
}
func RenderUI(state *State) int {
selectedClueCells := []int{}
if state.SelectedClue != nil {
selectedClueCells = state.SelectedClue.Cells
}
uiHeight := 0
fmt.Printf("\r\nTITLE: %s\r\n", state.Puzzle.Title)
uiHeight += 2
fmt.Printf("AUTHOR: %s\r\n", state.Puzzle.Author)
uiHeight += 1
fmt.Print(RenderPuzzle(state.PuzzleState, state.Puzzle.Width, state.Puzzle.Height, selectedClueCells, state.SelectedCell) + "\r\n")
uiHeight += state.Puzzle.Height + 3
if state.SelectedClue != nil {
if state.SelectedClue.Direction == puz.DirectionAcross {
fmt.Printf("%d-across: %s\r\n\r\n", state.SelectedClue.Number, state.SelectedClue.Clue)
} else {
fmt.Printf("%d-down: %s\r\n\r\n", state.SelectedClue.Number, state.SelectedClue.Clue)
}
uiHeight += 2
}
fmt.Printf("%s\r\n", state.Puzzle.Copyright)
uiHeight += 1
if state.LastKeySequence != "" && state.DebugMode {
fmt.Printf("\r\n%s\r\n", AnsiDimmed(state.LastKeySequence))
uiHeight += 2
}
if state.Goodbye {
fmt.Print("\r\nsee ya\r\n")
uiHeight += 2
}
return uiHeight
}
func main() { func main() {
fmt.Print("\x1b[?25l")
defer func() {
fmt.Print("\x1b[?25h")
}()
termState, err := term.MakeRaw(int(os.Stdin.Fd()))
if err != nil {
log.Fatal(err)
}
defer term.Restore(int(os.Stdin.Fd()), termState)
defer func() { defer func() {
if r := recover(); r != nil { if r := recover(); r != nil {
fmt.Print("!!! PROGRAM CRASHED !!! Error:\r\n", r, "\r\n") fmt.Print("!!! PROGRAM CRASHED !!! Error:\r\n", r, "\r\n")
@@ -252,6 +34,18 @@ func main() {
log.Fatal(err) log.Fatal(err)
} }
fmt.Print("\x1b[?25l")
defer func() {
fmt.Print("\x1b[?25h")
}()
termState, err := term.MakeRaw(int(os.Stdin.Fd()))
if err != nil {
log.Fatal(err)
}
defer term.Restore(int(os.Stdin.Fd()), termState)
state := State{ state := State{
Puzzle: puzzle, Puzzle: puzzle,
PuzzleState: []rune(puzzle.State), PuzzleState: []rune(puzzle.State),
@@ -262,14 +56,28 @@ func main() {
state.DebugMode = *debugMode state.DebugMode = *debugMode
} }
uiHeight := RenderUI(&state) uiHeight := state.RenderUI()
scanner := bufio.NewScanner(os.Stdin)
scanner.Split(func(data []byte, atEOF bool) (advance int, token []byte, err error) {
if len(data) == 0 {
return 0, nil, bufio.ErrFinalToken
}
if data[0] == 0x1b {
if len(data) < 3 {
return 0, nil, nil
}
return 3, data[0:3], nil
}
return 1, data[0:1], nil
})
for { for {
buffer := make([]byte, 3) scanner.Scan()
_, err = os.Stdin.Read(buffer) buffer := scanner.Bytes()
if err != nil {
log.Fatal(err)
}
state.LastKeySequence = fmt.Sprintf("%v", buffer) state.LastKeySequence = fmt.Sprintf("%v", buffer)
@@ -279,7 +87,7 @@ func main() {
state.Goodbye = true state.Goodbye = true
fmt.Printf("\r\x1b[%dA", uiHeight) fmt.Printf("\r\x1b[%dA", uiHeight)
fmt.Print("\x1b[J") fmt.Print("\x1b[J")
RenderUI(&state) state.RenderUI()
return return
} }
@@ -316,6 +124,16 @@ func main() {
state.SelectedCell = state.SelectedClue.Cells[i+1] state.SelectedCell = state.SelectedClue.Cells[i+1]
} }
} }
if state.GridFilled() {
if state.CheckPuzzle() {
state.SolveState = 2
} else {
state.SolveState = 1
}
} else {
state.SolveState = 0
}
} }
if buffer[0] == 0x7f { if buffer[0] == 0x7f {
@@ -335,33 +153,35 @@ func main() {
state.NextWord() state.NextWord()
} }
if (buffer[2] == 68 || buffer[2] == 67) && state.SelectedClue != nil && state.SelectedClue.Direction == puz.DirectionDown { if string(buffer[0:2]) == "\x1b[" {
state.SelectedClue = state.Puzzle.Cells[state.SelectedCell][0] if (buffer[2] == 68 || buffer[2] == 67) && state.SelectedClue != nil && state.SelectedClue.Direction == puz.DirectionDown {
} else if (buffer[2] == 65 || buffer[2] == 66) && state.SelectedClue != nil && state.SelectedClue.Direction == puz.DirectionAcross {
state.SelectedClue = state.Puzzle.Cells[state.SelectedCell][1]
} else {
switch buffer[2] {
case 68: // left
state.MoveCursor(3)
case 67: // right
state.MoveCursor(1)
case 65: // up
state.MoveCursor(0)
case 66: // down
state.MoveCursor(2)
}
switch state.SelectedClue.Direction {
case puz.DirectionAcross:
state.SelectedClue = state.Puzzle.Cells[state.SelectedCell][0] state.SelectedClue = state.Puzzle.Cells[state.SelectedCell][0]
case puz.DirectionDown: } 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.SelectedClue = state.Puzzle.Cells[state.SelectedCell][1]
} else {
switch buffer[2] {
case 68: // left
state.MoveCursor(3)
case 67: // right
state.MoveCursor(1)
case 65: // up
state.MoveCursor(0)
case 66: // down
state.MoveCursor(2)
}
switch state.SelectedClue.Direction {
case puz.DirectionAcross:
state.SelectedClue = state.Puzzle.Cells[state.SelectedCell][0]
case puz.DirectionDown:
state.SelectedClue = state.Puzzle.Cells[state.SelectedCell][1]
}
} }
} }
fmt.Printf("\r\x1b[%dA", uiHeight) fmt.Printf("\r\x1b[%dA", uiHeight)
fmt.Print("\x1b[J") fmt.Print("\x1b[J")
uiHeight = RenderUI(&state) uiHeight = state.RenderUI()
} }
} }