RosettaCodeData/Task/Word-search/Go/word-search.go
2023-07-01 13:44:08 -04:00

189 lines
4.5 KiB
Go

package main
import (
"bufio"
"fmt"
"log"
"math/rand"
"os"
"regexp"
"strings"
"time"
)
var dirs = [][]int{{1, 0}, {0, 1}, {1, 1}, {1, -1}, {-1, 0}, {0, -1}, {-1, -1}, {-1, 1}}
const (
nRows = 10
nCols = nRows
gridSize = nRows * nCols
minWords = 25
)
var (
re1 = regexp.MustCompile(fmt.Sprintf("^[a-z]{3,%d}$", nRows))
re2 = regexp.MustCompile("[^A-Z]")
)
type grid struct {
numAttempts int
cells [nRows][nCols]byte
solutions []string
}
func check(err error) {
if err != nil {
log.Fatal(err)
}
}
func readWords(fileName string) []string {
file, err := os.Open(fileName)
check(err)
defer file.Close()
var words []string
scanner := bufio.NewScanner(file)
for scanner.Scan() {
word := strings.ToLower(strings.TrimSpace(scanner.Text()))
if re1.MatchString(word) {
words = append(words, word)
}
}
check(scanner.Err())
return words
}
func createWordSearch(words []string) *grid {
var gr *grid
outer:
for i := 1; i < 100; i++ {
gr = new(grid)
messageLen := gr.placeMessage("Rosetta Code")
target := gridSize - messageLen
cellsFilled := 0
rand.Shuffle(len(words), func(i, j int) {
words[i], words[j] = words[j], words[i]
})
for _, word := range words {
cellsFilled += gr.tryPlaceWord(word)
if cellsFilled == target {
if len(gr.solutions) >= minWords {
gr.numAttempts = i
break outer
} else { // grid is full but we didn't pack enough words, start over
break
}
}
}
}
return gr
}
func (gr *grid) placeMessage(msg string) int {
msg = strings.ToUpper(msg)
msg = re2.ReplaceAllLiteralString(msg, "")
messageLen := len(msg)
if messageLen > 0 && messageLen < gridSize {
gapSize := gridSize / messageLen
for i := 0; i < messageLen; i++ {
pos := i*gapSize + rand.Intn(gapSize)
gr.cells[pos/nCols][pos%nCols] = msg[i]
}
return messageLen
}
return 0
}
func (gr *grid) tryPlaceWord(word string) int {
randDir := rand.Intn(len(dirs))
randPos := rand.Intn(gridSize)
for dir := 0; dir < len(dirs); dir++ {
dir = (dir + randDir) % len(dirs)
for pos := 0; pos < gridSize; pos++ {
pos = (pos + randPos) % gridSize
lettersPlaced := gr.tryLocation(word, dir, pos)
if lettersPlaced > 0 {
return lettersPlaced
}
}
}
return 0
}
func (gr *grid) tryLocation(word string, dir, pos int) int {
r := pos / nCols
c := pos % nCols
le := len(word)
// check bounds
if (dirs[dir][0] == 1 && (le+c) > nCols) ||
(dirs[dir][0] == -1 && (le-1) > c) ||
(dirs[dir][1] == 1 && (le+r) > nRows) ||
(dirs[dir][1] == -1 && (le-1) > r) {
return 0
}
overlaps := 0
// check cells
rr := r
cc := c
for i := 0; i < le; i++ {
if gr.cells[rr][cc] != 0 && gr.cells[rr][cc] != word[i] {
return 0
}
cc += dirs[dir][0]
rr += dirs[dir][1]
}
// place
rr = r
cc = c
for i := 0; i < le; i++ {
if gr.cells[rr][cc] == word[i] {
overlaps++
} else {
gr.cells[rr][cc] = word[i]
}
if i < le-1 {
cc += dirs[dir][0]
rr += dirs[dir][1]
}
}
lettersPlaced := le - overlaps
if lettersPlaced > 0 {
sol := fmt.Sprintf("%-10s (%d,%d)(%d,%d)", word, c, r, cc, rr)
gr.solutions = append(gr.solutions, sol)
}
return lettersPlaced
}
func printResult(gr *grid) {
if gr.numAttempts == 0 {
fmt.Println("No grid to display")
return
}
size := len(gr.solutions)
fmt.Println("Attempts:", gr.numAttempts)
fmt.Println("Number of words:", size)
fmt.Println("\n 0 1 2 3 4 5 6 7 8 9")
for r := 0; r < nRows; r++ {
fmt.Printf("\n%d ", r)
for c := 0; c < nCols; c++ {
fmt.Printf(" %c ", gr.cells[r][c])
}
}
fmt.Println("\n")
for i := 0; i < size-1; i += 2 {
fmt.Printf("%s %s\n", gr.solutions[i], gr.solutions[i+1])
}
if size%2 == 1 {
fmt.Println(gr.solutions[size-1])
}
}
func main() {
rand.Seed(time.Now().UnixNano())
unixDictPath := "/usr/share/dict/words"
printResult(createWordSearch(readWords(unixDictPath)))
}