Skip to content

Commit

Permalink
add cli
Browse files Browse the repository at this point in the history
  • Loading branch information
deanveloper committed Sep 13, 2021
1 parent 75cded2 commit 48b33f5
Show file tree
Hide file tree
Showing 10 changed files with 263 additions and 52 deletions.
11 changes: 11 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# gridspech-go

`gridspech-go` is an implementation of the puzzle game [gridspech](https://krackocloud.itch.io/gridspech) written in Go.

This implementation was written to make a solver, which the package can be found at [`gridspech-go/solve`](solve). The solver was written to find unintended solutions to puzzles, so don't use the solver until you have figured out the puzzle on your own :)

## CLI Installation

Binaries for common environments can be found on the [releases page](https://github.com/deanveloper/gridspech-go/releases). After this, you can add it to your PATH.

To install the CLI from source, first make sure you have the [Go compiler](https://golang.org/dl/) installed. Then, run `go install -o gs-solve "github.com/deanveloper/gridspech-go/solve/cli@latest"`. The binary will appear in $GOBIN, which by default is `~/go/bin`
2 changes: 2 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
module github.com/deanveloper/gridspech-go

go 1.16

require github.com/pborman/getopt/v2 v2.1.0
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
github.com/dustin/go-broadcast v0.0.0-20171205050544-f664265f5a66 h1:QnnoVdChKs+GeTvN4rPYTW6b5U6M3HMEvQ/+x4IGtfY=
github.com/dustin/go-broadcast v0.0.0-20171205050544-f664265f5a66/go.mod h1:kTEh6M2J/mh7nsskr28alwLCXm/DSG5OSA/o31yy2XU=
github.com/pborman/getopt/v2 v2.1.0 h1:eNfR+r+dWLdWmV8g5OlpyrTYHkhVNxHBdN2cCrJmOEA=
github.com/pborman/getopt/v2 v2.1.0/go.mod h1:4NtW75ny4eBw9fO1bhtNdYTlZKYX5/tBLtsOpwKIKd0=
132 changes: 132 additions & 0 deletions solve/cli/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
package main

import (
"fmt"
"io"
"log"
"os"

"github.com/deanveloper/gridspech-go"
"github.com/deanveloper/gridspech-go/solve"
"github.com/pborman/getopt/v2"
)

const (
outputString = "lines"
outputJSON = "json"
)

var (
helpFlag = getopt.BoolLong("help", 'h', "display help")
maxColors = getopt.IntLong("maxcolors", 'm', 0, "the total number of colors available for this level", "2")
solveTiles = getopt.ListLong("tiles", 't', "solve specific tiles. a comma-separated list of space-separated coordinates")
solveGoals = getopt.BoolLong("goals", 'g', "solve all goal tiles")
solveCrowns = getopt.BoolLong("crowns", 'c', "solve all crown tiles")
solveDots = getopt.BoolLong("dots", 'd', "solve all dot tiles")
solveJoins = getopt.BoolLong("joins", 'j', "solve all join tiles")
solveAll = getopt.BoolLong("all", 'a', "solve all tiles")
jsonOutput = getopt.EnumLong("format", 'f', []string{outputString, outputJSON}, "", "output format (lines or json)")
)

func solutionsFromFlags(solver solve.GridSolver) <-chan gridspech.TileSet {
var ch <-chan gridspech.TileSet
if *solveAll {
ch = solver.SolveAllTiles()
} else {
{
tempCh := make(chan gridspech.TileSet, 1)
tempCh <- gridspech.NewTileSet()
close(tempCh)
ch = tempCh
}

if getopt.IsSet('t') {
tiles := parseCoords(*solveTiles)
ch = solve.MergeSolutionsIters(ch, solver.SolveTiles(tiles...))
}
if *solveGoals {
ch = solve.MergeSolutionsIters(ch, solver.SolveGoals())
}
if *solveJoins {
ch = solve.MergeSolutionsIters(ch, solver.SolveGoals())
}
if *solveDots {
ch = solve.MergeSolutionsIters(ch, solver.SolveGoals())
}
if *solveCrowns {
ch = solve.MergeSolutionsIters(ch, solver.SolveGoals())
}
}
return ch
}

func parseCoords(coordsStr []string) []gridspech.TileCoord {
var coords []gridspech.TileCoord
for _, coordStr := range coordsStr {
var x, y int
n, err := fmt.Sscanf(coordStr, "%d %d", &x, &y)
if err != nil || n != 2 {
log.Printf("skipping invalid coord %s\n", coordStr)
continue
}
coords = append(coords, gridspech.TileCoord{X: x, Y: y})
}
return coords
}

func main() {
getopt.HelpColumn = 22
getopt.SetUsage(func() {
fmt.Fprintf(
os.Stderr, "Usage: %v %v\n",
getopt.CommandLine.Program(),
getopt.CommandLine.UsageLine(),
)
fmt.Fprintln(os.Stderr, "Standard input will be interpreted as the level to solve.")
getopt.CommandLine.PrintOptions(os.Stderr)
})
getopt.Parse()
if !getopt.IsSet('a') && !getopt.IsSet('t') && !getopt.IsSet('g') && !getopt.IsSet('c') && !getopt.IsSet('d') && !getopt.IsSet('j') {
getopt.Usage()
return
}
if *helpFlag {
getopt.Usage()
return
}

const maxLevelLen, bufLen = 10000, 100
var buf [bufLen]byte
var levelBytes []byte

for i := 0; i < maxLevelLen/bufLen; i++ {
n, err := os.Stdin.Read(buf[:])
if err != nil {
if err != io.EOF {
log.Fatalln("error:", err)
} else {
break
}
}
levelBytes = append(levelBytes, buf[:n]...)
}
if len(levelBytes) == maxLevelLen {
log.Fatalln("standard input is over 10000 bytes... are you sure it is a gridspech level?")
}

level := string(levelBytes)
solver := solve.NewGridSolver(gridspech.MakeGridFromString(level, *maxColors))

solutions := solutionsFromFlags(solver)

first := true
for solution := range solutions {
if !first {
fmt.Println()
}

newGrid := solver.Grid.Clone()
newGrid.ApplyTileSet(solution)
fmt.Println(newGrid)
}
}
2 changes: 1 addition & 1 deletion solve/crown.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ func (g GridSolver) SolveCrowns() <-chan gs.TileSet {

// now merge them all together
for i := 1; i < len(crownTiles); i++ {
mergedIter := mergeSolutionsIters(tilesToSolutions[i-1], tilesToSolutions[i])
mergedIter := MergeSolutionsIters(tilesToSolutions[i-1], tilesToSolutions[i])
tilesToSolutions[i] = mergedIter
}

Expand Down
2 changes: 1 addition & 1 deletion solve/dots.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ func (g GridSolver) SolveDots() <-chan gs.TileSet {

// now merge them all together
for i := 1; i < len(dotTiles); i++ {
mergedIter := mergeSolutionsIters(tilesToSolutions[i-1], tilesToSolutions[i])
mergedIter := MergeSolutionsIters(tilesToSolutions[i-1], tilesToSolutions[i])
uniqueIter := filterUnique(mergedIter)
tilesToSolutions[i] = uniqueIter
}
Expand Down
2 changes: 1 addition & 1 deletion solve/join.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ func (g GridSolver) SolveJoins() <-chan gs.TileSet {

// now merge them all together
for i := 1; i < len(joinTiles); i++ {
mergedIter := mergeSolutionsIters(tilesToSolutions[i-1], tilesToSolutions[i])
mergedIter := MergeSolutionsIters(tilesToSolutions[i-1], tilesToSolutions[i])
tilesToSolutions[i] = mergedIter
}

Expand Down
89 changes: 89 additions & 0 deletions solve/solve.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
package solve

import (
"fmt"

gs "github.com/deanveloper/gridspech-go"
)

// SolveAllTiles returns a channel which will return a TileSet of all tiles in g.
func (g GridSolver) SolveAllTiles() <-chan gs.TileSet {
solutionIter := make(chan gs.TileSet)

go func() {
defer close(solutionIter)

for goalsAndDots := range MergeSolutionsIters(g.SolveGoals(), g.SolveDots()) {
newGrid := g.Clone()
newGrid.Grid.ApplyTileSet(goalsAndDots)
newGrid.UnknownTiles.RemoveAll(goalsAndDots.ToTileCoordSet())

for joinsSolution := range newGrid.SolveJoins() {
joinsSolved := newGrid.Clone()
joinsSolved.Grid.ApplyTileSet(joinsSolution)
joinsSolved.UnknownTiles.RemoveAll(joinsSolution.ToTileCoordSet())

for crownsSolution := range joinsSolved.SolveCrowns() {
crownsSolved := joinsSolved.Clone()
crownsSolved.Grid.ApplyTileSet(crownsSolution)
crownsSolved.UnknownTiles.RemoveAll(crownsSolution.ToTileCoordSet())

if crownsSolved.Grid.Valid() {
var merged gs.TileSet
merged.Merge(goalsAndDots)
merged.Merge(joinsSolution)
merged.Merge(crownsSolution)
solutionIter <- merged
}
}
}
}
}()

return solutionIter
}

// SolveTiles returns a channel of possible solutions for the given tiles.
func (g GridSolver) SolveTiles(tiles ...gs.TileCoord) <-chan gs.TileSet {

if len(tiles) == 0 {
ch := make(chan gs.TileSet, 1)
ch <- gs.NewTileSet()
close(ch)
return ch
}

tilesToSolutions := make([]<-chan gs.TileSet, len(tiles))
for i, tile := range tiles {
tilesToSolutions[i] = g.solveTile(*g.Grid.TileAtCoord(tile))
}

// now merge them all together
for i := 1; i < len(tiles); i++ {
mergedIter := MergeSolutionsIters(tilesToSolutions[i-1], tilesToSolutions[i])
uniqueIter := filterUnique(mergedIter)
tilesToSolutions[i] = uniqueIter
}

return tilesToSolutions[len(tilesToSolutions)-1]
}

func (g GridSolver) solveTile(t gs.Tile) <-chan gs.TileSet {
switch t.Data.Type {
case gs.TypeHole, gs.TypeBlank:
ch := make(chan gs.TileSet, 1)
ch <- gs.NewTileSet()
close(ch)
return ch
case gs.TypeGoal:
return filterHasTile(g.SolveGoals(), t.Coord)
case gs.TypeCrown:
return g.SolveCrown(t.Coord)
case gs.TypeDot1, gs.TypeDot2, gs.TypeDot3:
return g.SolveDot(t)
case gs.TypeJoin1, gs.TypeJoin2:
return g.SolveJoin(t)
default:
panic(fmt.Sprintf("invalid type %v", t.Data.Type))
}
}
42 changes: 0 additions & 42 deletions solve/solveall.go

This file was deleted.

29 changes: 24 additions & 5 deletions solve/transformers.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,12 @@ package solve

import gs "github.com/deanveloper/gridspech-go"

func mergeSolutionsIters(sols1, sols2 <-chan gs.TileSet) <-chan gs.TileSet {
iter := make(chan gs.TileSet, 50)
// MergeSolutionsIters makes pairs of solutions from sols1 and sols2 into
// a single solution, then returns a channel of the merged pairs of solutions.
//
// A solution pair will only be sent if any tiles which appear in both solutions are equal.
func MergeSolutionsIters(sols1, sols2 <-chan gs.TileSet) <-chan gs.TileSet {
iter := make(chan gs.TileSet, 20)

go func() {
// read sols2 into a slice
Expand All @@ -22,7 +26,7 @@ func mergeSolutionsIters(sols1, sols2 <-chan gs.TileSet) <-chan gs.TileSet {
// do not merge if they have any tiles with unmatched colors
for _, t1 := range sol1.Slice() {
for _, t2 := range sol2.Slice() {
if t1.Coord == t2.Coord && t1.Data.Color != t2.Data.Color {
if t1.Coord == t2.Coord && t1.Data != t2.Data {
continue nextSolution
}
}
Expand All @@ -40,7 +44,7 @@ func mergeSolutionsIters(sols1, sols2 <-chan gs.TileSet) <-chan gs.TileSet {
}

func filterUnique(in <-chan gs.TileSet) <-chan gs.TileSet {
filtered := make(chan gs.TileSet, 200)
filtered := make(chan gs.TileSet, 20)

go func() {
var alreadySeen []gs.TileSet
Expand Down Expand Up @@ -69,7 +73,7 @@ func filterValid(
current gs.Tile,
sols <-chan gs.TileSet,
) <-chan gs.TileSet {
filtered := make(chan gs.TileSet, 200)
filtered := make(chan gs.TileSet, 20)

go func() {
defer close(filtered)
Expand All @@ -93,6 +97,21 @@ func filterValid(
return filtered
}

func filterHasTile(in <-chan gs.TileSet, coord gs.TileCoord) <-chan gs.TileSet {
filtered := make(chan gs.TileSet, 20)

go func() {
defer close(filtered)
for solution := range in {
if solution.ToTileCoordSet().Has(coord) {
filtered <- solution
}
}
}()

return filtered
}

func decorateSetBorder(g GridSolver, shapeColor gs.TileColor, tileSet gs.TileSet) <-chan gs.TileSet {
iter := make(chan gs.TileSet)
go func() {
Expand Down

0 comments on commit 48b33f5

Please sign in to comment.