diff --git a/README.md b/README.md index 04a16a0..c0b4b97 100644 --- a/README.md +++ b/README.md @@ -5,4 +5,4 @@ todo - check puzzle when grid is filled - fix bug with that one 6x6 puzzle - correctly calculate uiHeight with multi-line clues -- handle multiple keypresses in one `read` +- timer diff --git a/game.go b/game.go new file mode 100644 index 0000000..652c75e --- /dev/null +++ b/game.go @@ -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() +} diff --git a/main.go b/main.go index e117eae..63ed657 100644 --- a/main.go +++ b/main.go @@ -1,237 +1,19 @@ package main import ( + "bufio" "flag" "fmt" "log" "os" "slices" - "strings" "unicode" "github.com/cjdenio/crossword/puz" "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() { - 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() { if r := recover(); r != nil { fmt.Print("!!! PROGRAM CRASHED !!! Error:\r\n", r, "\r\n") @@ -252,6 +34,18 @@ func main() { 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{ Puzzle: puzzle, PuzzleState: []rune(puzzle.State), @@ -262,14 +56,28 @@ func main() { 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 { - buffer := make([]byte, 3) - _, err = os.Stdin.Read(buffer) - if err != nil { - log.Fatal(err) - } + scanner.Scan() + buffer := scanner.Bytes() state.LastKeySequence = fmt.Sprintf("%v", buffer) @@ -279,7 +87,7 @@ func main() { state.Goodbye = true fmt.Printf("\r\x1b[%dA", uiHeight) fmt.Print("\x1b[J") - RenderUI(&state) + state.RenderUI() return } @@ -316,6 +124,16 @@ func main() { 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 { @@ -335,33 +153,35 @@ func main() { state.NextWord() } - if (buffer[2] == 68 || buffer[2] == 67) && state.SelectedClue != nil && state.SelectedClue.Direction == puz.DirectionDown { - state.SelectedClue = state.Puzzle.Cells[state.SelectedCell][0] - } 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: + if string(buffer[0:2]) == "\x1b[" { + if (buffer[2] == 68 || buffer[2] == 67) && state.SelectedClue != nil && state.SelectedClue.Direction == puz.DirectionDown { 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] + } 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.Print("\x1b[J") - uiHeight = RenderUI(&state) + uiHeight = state.RenderUI() } }