├── .gitignore ├── LICENSE.md ├── README.md ├── anneal.go ├── cmd ├── bench │ └── main.go ├── canonical │ └── main.go ├── enumerate │ └── main.go ├── example │ └── main.go ├── forty │ └── main.go ├── generate │ └── main.go ├── graph │ └── main.go ├── impossible │ └── main.go ├── multi │ └── main.go ├── profile │ └── main.go ├── render │ └── main.go ├── solve │ └── main.go ├── solver │ └── main.go ├── static │ └── main.go ├── universe │ └── main.go └── unsolver │ └── main.go ├── config.go ├── cpp ├── .gitignore ├── Makefile └── src │ ├── bb.cpp │ ├── bb.h │ ├── board.cpp │ ├── board.h │ ├── cluster.cpp │ ├── cluster.h │ ├── config.h │ ├── enumerator.cpp │ ├── enumerator.h │ ├── main.cpp │ ├── move.cpp │ ├── move.h │ ├── piece.cpp │ ├── piece.h │ ├── solver.cpp │ └── solver.h ├── enumerator.go ├── generator.go ├── graph.go ├── memo.go ├── model.go ├── render.go ├── server ├── .gitignore └── main.py ├── solver.go ├── static.go ├── static_test.go ├── unsolver.go ├── util.go └── web ├── app.js ├── index.html └── style.css /.gitignore: -------------------------------------------------------------------------------- 1 | *.png 2 | 3 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (C) 2018 Michael Fogleman 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Rush Hour (the puzzle) 2 | 3 | This repo contains a bunch of stuff related to [Rush Hour](https://en.wikipedia.org/wiki/Rush_Hour_(puzzle)), a sliding block puzzle. You may also know this game from one of its iOS implementations, such as [Unblock Me](https://itunes.apple.com/us/app/unblock-me/id315019111?mt=8). 4 | 5 | ### Read the Article 6 | 7 | https://www.michaelfogleman.com/rush/ 8 | 9 | ### Play Online 10 | 11 | https://www.michaelfogleman.com/static/rush/ 12 | 13 | ### Overview 14 | 15 | The Go code can solve puzzles, render puzzles to PNG, generate puzzles, and more. 16 | 17 | The C++ code "solves" the entire game - identifying all "interesting" puzzles that are possible. Read the article for more information. A database of puzzles is available. 18 | 19 | The JavaScript code is for the online player. 20 | 21 | ![51](https://www.michaelfogleman.com/static/rush/puzzle51.png) 22 | -------------------------------------------------------------------------------- /anneal.go: -------------------------------------------------------------------------------- 1 | package rush 2 | 3 | import ( 4 | "fmt" 5 | "math" 6 | "math/rand" 7 | "time" 8 | ) 9 | 10 | func anneal(state *Board, maxTemp, minTemp float64, steps int) *Board { 11 | start := time.Now() 12 | factor := -math.Log(maxTemp / minTemp) 13 | state = state.Copy() 14 | bestState := state.Copy() 15 | bestEnergy := state.Energy() 16 | bestTime := start 17 | previousEnergy := bestEnergy 18 | rate := steps / 1000 19 | for step := 0; step < steps; step++ { 20 | pct := float64(step) / float64(steps-1) 21 | temp := maxTemp * math.Exp(factor*pct) 22 | if step%rate == 0 { 23 | showAnnealProgress( 24 | step, steps, temp, bestEnergy, time.Since(start).Seconds()) 25 | } 26 | undo := state.Mutate() 27 | energy := state.Energy() 28 | change := energy - previousEnergy 29 | if change > 0 && math.Exp(-change/temp) < rand.Float64() { 30 | undo() 31 | } else { 32 | previousEnergy = energy 33 | if energy < bestEnergy { 34 | bestEnergy = energy 35 | bestState = state.Copy() 36 | bestTime = time.Now() 37 | } 38 | } 39 | if time.Since(bestTime).Seconds() > 15 { 40 | fmt.Println() 41 | return bestState 42 | } 43 | } 44 | showAnnealProgress( 45 | steps, steps, minTemp, bestEnergy, time.Since(start).Seconds()) 46 | fmt.Println() 47 | return bestState 48 | } 49 | 50 | func showAnnealProgress(i, n int, t, e, d float64) { 51 | pct := int(100 * float64(i) / float64(n)) 52 | fmt.Printf(" %3d%% [", pct) 53 | for p := 0; p < 100; p += 3 { 54 | if pct > p { 55 | fmt.Print("=") 56 | } else { 57 | fmt.Print(" ") 58 | } 59 | } 60 | fmt.Printf("] %.1f %.2f %.3fs \r", t, e, d) 61 | } 62 | -------------------------------------------------------------------------------- /cmd/bench/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "time" 7 | 8 | . "github.com/fogleman/rush" 9 | ) 10 | 11 | func main() { 12 | board, err := NewBoard([]string{ 13 | "BCDDE.", 14 | "BCF.EG", 15 | "B.FAAG", 16 | "HHHI.G", 17 | "..JIKK", 18 | "LLJMM.", 19 | // "BB.C..", 20 | // ".D.CEE", 21 | // ".DAAFG", 22 | // "H.IIFG", 23 | // "H.JKK.", 24 | // "LLJ...", 25 | }) 26 | if err != nil { 27 | log.Fatal(err) 28 | } 29 | // fmt.Println(board.ReachableStates()) 30 | start := time.Now() 31 | board.Unsolve() 32 | fmt.Println(time.Since(start)) 33 | // var moves []Move 34 | // memo := NewMemo() 35 | // for i := 0; i < 5000000; i++ { 36 | // memo.Add(board.MemoKey(), 0) 37 | // moves = board.Moves(moves) 38 | // board.DoMove(moves[rand.Intn(len(moves))]) 39 | // } 40 | // fmt.Println(memo.Size()) 41 | } 42 | -------------------------------------------------------------------------------- /cmd/canonical/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "math/rand" 6 | "time" 7 | 8 | "github.com/fogleman/rush" 9 | ) 10 | 11 | func main() { 12 | rand.Seed(time.Now().UTC().UnixNano()) 13 | 14 | board := rush.NewRandomBoard(6, 6, 2, 2, 8, 0) 15 | 16 | fmt.Println(board) 17 | fmt.Println() 18 | 19 | canonical := board.Canonicalize() 20 | 21 | fmt.Println(canonical) 22 | } 23 | -------------------------------------------------------------------------------- /cmd/enumerate/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | 6 | . "github.com/fogleman/rush" 7 | ) 8 | 9 | const ( 10 | // W = 4 11 | // H = 4 12 | // Py = 1 13 | // Px = 2 14 | 15 | // W = 5 16 | // H = 5 17 | // Py = 2 18 | // Px = 3 19 | 20 | W = 6 21 | H = 6 22 | Py = 2 23 | Px = 4 24 | 25 | P = Py*W + Px 26 | ) 27 | 28 | type Enumerator struct { 29 | Board *Board 30 | Seen map[string]bool 31 | Memo *Memo 32 | Solver *Solver 33 | HardestSolution Solution 34 | HardestBoard *Board 35 | Canonical bool 36 | CanonicalKey MemoKey 37 | Count int 38 | } 39 | 40 | func NewEnumerator(board *Board) *Enumerator { 41 | e := &Enumerator{} 42 | e.Board = board 43 | e.Seen = make(map[string]bool) 44 | return e 45 | } 46 | 47 | func (e *Enumerator) hardestSearch(previousPiece int) { 48 | board := e.Board 49 | 50 | if !e.Memo.Add(board.MemoKey(), 0) { 51 | return 52 | } 53 | 54 | solution := e.Solver.UnsafeSolve() 55 | delta := solution.NumMoves - e.HardestSolution.NumMoves 56 | if delta > 0 || (delta == 0 && board.MemoKey().Less(e.HardestBoard.MemoKey(), true)) { 57 | e.HardestSolution = solution 58 | e.HardestBoard = board.Copy() 59 | } 60 | 61 | for _, move := range board.Moves(nil) { 62 | if move.Piece == previousPiece { 63 | continue 64 | } 65 | board.DoMove(move) 66 | e.hardestSearch(move.Piece) 67 | board.UndoMove(move) 68 | } 69 | } 70 | 71 | func (e *Enumerator) HardestSearch() { 72 | e.Memo = NewMemo() 73 | e.Solver = NewSolver(e.Board) 74 | e.HardestBoard = e.Board.Copy() 75 | e.HardestSolution = e.Solver.Solve() 76 | e.hardestSearch(-1) 77 | e.HardestBoard.SortPieces() 78 | } 79 | 80 | func (e *Enumerator) canonicalSearch(previousPiece int) { 81 | if !e.Canonical { 82 | return 83 | } 84 | 85 | board := e.Board 86 | 87 | if !e.Memo.Add(board.MemoKey(), 0) { 88 | return 89 | } 90 | 91 | if board.MemoKey().Less(&e.CanonicalKey, false) { 92 | e.Canonical = false 93 | return 94 | } 95 | 96 | for _, move := range board.Moves(nil) { 97 | if move.Piece == 0 { 98 | continue 99 | } 100 | if move.Piece == previousPiece { 101 | continue 102 | } 103 | board.DoMove(move) 104 | e.canonicalSearch(move.Piece) 105 | board.UndoMove(move) 106 | } 107 | } 108 | 109 | func (e *Enumerator) CanonicalSearch() { 110 | e.Memo = NewMemo() 111 | e.Canonical = true 112 | e.CanonicalKey = *e.Board.MemoKey() 113 | e.canonicalSearch(-1) 114 | } 115 | 116 | func (e *Enumerator) place(after int) { 117 | board := e.Board 118 | 119 | if board.HasFullRowOrCol() { 120 | return 121 | } 122 | 123 | e.CanonicalSearch() 124 | if !e.Canonical { 125 | return 126 | } 127 | 128 | e.HardestSearch() 129 | hardest := e.HardestBoard 130 | solution := e.HardestSolution 131 | 132 | if solution.NumMoves == 0 { 133 | return 134 | } 135 | 136 | if solution.NumMoves >= 1 { 137 | key := hardest.Hash() 138 | _, seen := e.Seen[key] 139 | if !seen { 140 | e.Seen[key] = true 141 | fmt.Printf("%02d %02d %s %d\n", solution.NumMoves, solution.NumSteps, key, solution.MemoSize) 142 | } else { 143 | e.Count++ 144 | } 145 | } 146 | 147 | w := board.Width 148 | h := board.Height 149 | i := len(board.Pieces) 150 | 151 | for o := Horizontal; o <= Vertical; o++ { 152 | for s := 2; s <= 3; s++ { 153 | xx := w 154 | yy := h 155 | if o == Horizontal { 156 | xx = W - s + 1 157 | } else { 158 | yy = H - s + 1 159 | } 160 | for y := 0; y < yy; y++ { 161 | if o == Horizontal && y == Py { 162 | continue 163 | } 164 | for x := 0; x < xx; x++ { 165 | p := y*W + x 166 | if p <= after { 167 | continue 168 | } 169 | if board.AddPiece(Piece{p, s, o}) { 170 | e.place(p) 171 | board.RemovePiece(i) 172 | } 173 | } 174 | } 175 | } 176 | } 177 | } 178 | 179 | func (e *Enumerator) Enumerate() { 180 | e.place(-1) 181 | } 182 | 183 | func main() { 184 | board := NewEmptyBoard(W, H) 185 | board.AddPiece(Piece{P, 2, Horizontal}) 186 | e := NewEnumerator(board) 187 | e.Enumerate() 188 | fmt.Println(len(e.Seen), e.Count) 189 | } 190 | -------------------------------------------------------------------------------- /cmd/example/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "strings" 7 | 8 | "github.com/fogleman/rush" 9 | ) 10 | 11 | func main() { 12 | // define the puzzle in ASCII 13 | desc := []string{ 14 | "BBBCDE", 15 | "FGGCDE", 16 | "F.AADE", 17 | "HHI...", 18 | ".JI.KK", 19 | ".JLLMM", 20 | } 21 | 22 | // parse and create a board 23 | board, err := rush.NewBoard(desc) 24 | if err != nil { 25 | log.Fatal(err) 26 | } 27 | 28 | // compute a solution 29 | solution := board.Solve() 30 | 31 | // print out solution information 32 | fmt.Printf("solvable: %t\n", solution.Solvable) 33 | fmt.Printf(" # moves: %d\n", solution.NumMoves) 34 | fmt.Printf(" # steps: %d\n", solution.NumSteps) 35 | 36 | // print out moves to solve puzzle 37 | moveStrings := make([]string, len(solution.Moves)) 38 | for i, move := range solution.Moves { 39 | moveStrings[i] = move.String() 40 | } 41 | fmt.Println(strings.Join(moveStrings, ", ")) 42 | 43 | // solvable: true 44 | // # moves: 49 45 | // # steps: 93 46 | // A-1, C+2, B+1, E+1, F-1, A-1, I-1, K-2, D+2, B+2, G+2, I-2, A+1, H+1, 47 | // F+4, A-1, H-1, I+2, B-2, E-1, G-3, C-1, D-2, I-1, H+4, F-1, J-1, K+2, 48 | // L-2, C+3, I+3, A+2, G+2, F-3, H-2, D+1, B+1, J-3, A-2, H-2, C-2, I-2, 49 | // K-4, C+1, I+1, M-2, D+2, E+3, A+4 50 | } 51 | -------------------------------------------------------------------------------- /cmd/forty/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | 7 | "github.com/fogleman/rush" 8 | ) 9 | 10 | func process(number int, desc []string) { 11 | board, err := rush.NewBoard(desc) 12 | if err != nil { 13 | log.Fatal(err) 14 | } 15 | solution := board.Solve() 16 | fmt.Println(solution) 17 | } 18 | 19 | func main() { 20 | for i, desc := range Levels { 21 | process(i+1, desc) 22 | } 23 | } 24 | 25 | var Levels = [][]string{ 26 | { 27 | "..B.CC", 28 | "..B...", 29 | "AAB...", 30 | "DDD..E", 31 | ".....E", 32 | ".....E", 33 | }, 34 | { 35 | "..B...", 36 | "..B..C", 37 | "..BAAC", 38 | "DDDE.C", 39 | "...EFF", 40 | "......", 41 | }, 42 | { 43 | "BCDDE.", 44 | "BCFFE.", 45 | "AAGH..", 46 | "..GHI.", 47 | "..JHI.", 48 | "..J...", 49 | }, 50 | { 51 | "......", 52 | "..B.C.", 53 | "AAB.CD", 54 | "EEE.CD", 55 | "FGH.II", 56 | "FGH.JJ", 57 | }, 58 | { 59 | "..BBB.", 60 | "CCC.D.", 61 | "AA..DE", 62 | "FFF.DE", 63 | "G.H.II", 64 | "G.H.JJ", 65 | }, 66 | { 67 | "......", 68 | "..B..C", 69 | "AAB..C", 70 | "D.BEEF", 71 | "DGGHIF", 72 | "JJJHIF", 73 | }, 74 | { 75 | "..BBC.", 76 | ".DDDCE", 77 | "..AACE", 78 | "..FGGG", 79 | "..FHII", 80 | "...HJJ", 81 | }, 82 | { 83 | "BBCDEE", 84 | "FFCDGH", 85 | ".IAAGH", 86 | ".I....", 87 | ".I.JJJ", 88 | "......", 89 | }, 90 | { 91 | "BCCD..", 92 | "B.ED.F", 93 | "AAE..F", 94 | "GGHHHI", 95 | "..J..I", 96 | "..J...", 97 | }, 98 | { 99 | ".BCCDD", 100 | ".B...E", 101 | ".B.AAE", 102 | ".F.GHH", 103 | ".F.GIJ", 104 | ".KKKIJ", 105 | }, 106 | { 107 | "BBBC.D", 108 | "..EC.D", 109 | "AAE..F", 110 | "..EGGF", 111 | "HHH.I.", 112 | ".JJ.I.", 113 | }, 114 | { 115 | "BBCDE.", 116 | "F.CDE.", 117 | "FAADEG", 118 | ".....G", 119 | ".HII.G", 120 | ".H.JJJ", 121 | }, 122 | { 123 | ".BBCCC", 124 | "DDDEFG", 125 | "AAHEFG", 126 | "I.HJJK", 127 | "ILL..K", 128 | "MMNNNK", 129 | }, 130 | { 131 | "BBC...", 132 | "D.CEEE", 133 | "D.AAF.", 134 | "....F.", 135 | "GGHHHI", 136 | ".....I", 137 | }, 138 | { 139 | "BCDDEE", 140 | "BC.FFF", 141 | "BG.AAH", 142 | "IG.JJH", 143 | "IKKKLM", 144 | "NNN.LM", 145 | }, 146 | { 147 | "...BCC", 148 | "...BDD", 149 | "AAEFGH", 150 | "IIEFGH", 151 | "JKKKLM", 152 | "JNNNLM", 153 | }, 154 | { 155 | "BBBCCD", 156 | "EEFGGD", 157 | "AAFH.I", 158 | "...HJI", 159 | "KLLHJ.", 160 | "KMMMNN", 161 | }, 162 | { 163 | ".BCCCD", 164 | ".B...D", 165 | ".AAEFG", 166 | "HHIEFG", 167 | "J.IKKG", 168 | "J.ILLL", 169 | }, 170 | { 171 | "..BCCC", 172 | "..B..D", 173 | "AAE..D", 174 | "FFE.GH", 175 | ".IJJGH", 176 | ".IKKKH", 177 | }, 178 | { 179 | "...BCC", 180 | "...B..", 181 | "AADE.F", 182 | "..DEGF", 183 | "HIIIGK", 184 | "H....K", 185 | }, 186 | { 187 | "BBCD..", 188 | "E.CD..", 189 | "EAAD..", 190 | "E..FFF", 191 | "GGG.HI", 192 | "JJ..HI", 193 | }, 194 | { 195 | ".BCCDE", 196 | ".BFGDE", 197 | "AAFGHI", 198 | "J.KLHI", 199 | "J.KL.I", 200 | "JMMMNN", 201 | }, 202 | { 203 | "B.C.DD", 204 | "B.CEEE", 205 | "FAAG.H", 206 | "FI.G.H", 207 | "FIJJKL", 208 | "MMNNKL", 209 | }, 210 | { 211 | "B.CDDD", 212 | "B.CE..", 213 | "BAAE..", 214 | "FFGG.H", 215 | ".....H", 216 | "IIJJ.H", 217 | }, 218 | { 219 | "BB.CDE", 220 | "...CDE", 221 | "FAAC.G", 222 | "F.HIIG", 223 | "JJH..G", 224 | "KKH...", 225 | }, 226 | { 227 | "B..C..", 228 | "B..CDD", 229 | "BAAE..", 230 | "..FEGG", 231 | "..FHHI", 232 | "..F..I", 233 | }, 234 | { 235 | "BBC.DD", 236 | "E.C...", 237 | "E.AAF.", 238 | "EGGGFH", 239 | "...IFH", 240 | "...IJJ", 241 | }, 242 | { 243 | "BBCCCD", 244 | "E..FFD", 245 | "E..AAG", 246 | "HHIIJG", 247 | "KKL.JG", 248 | "..LMMM", 249 | }, 250 | { 251 | "BBC.DE", 252 | "..C.DE", 253 | "..CAAE", 254 | "...FGG", 255 | "HIIF..", 256 | "H..F..", 257 | }, 258 | { 259 | "B.CCC.", 260 | "BDDDEF", 261 | "AAGHEF", 262 | "IIGHEF", 263 | "...JKK", 264 | ".LLJ..", 265 | }, 266 | { 267 | "BCDDEF", 268 | "BC..EF", 269 | "GAAH.I", 270 | "GJJH.I", 271 | "G.KLLL", 272 | "MMKNNN", 273 | }, 274 | { 275 | "BBBCDE", 276 | "FGGCDE", 277 | "F.AADE", 278 | "HHI...", 279 | ".JIKK.", 280 | ".JLLMM", 281 | }, 282 | { 283 | "BBCCCD", 284 | "E.FFGD", 285 | "E.AAGH", 286 | "IIJKKH", 287 | "LLJM..", 288 | "NNNM..", 289 | }, 290 | { 291 | "BCCD.E", 292 | "B.FD.E", 293 | "AAFD.G", 294 | "HHIIJG", 295 | "..K.J.", 296 | "..KLLL", 297 | }, 298 | { 299 | "BB.C.D", 300 | "EF.C.D", 301 | "EFAAGH", 302 | "IJJKGH", 303 | "I.LKG.", 304 | "I.LMM.", 305 | }, 306 | { 307 | "BCCD..", 308 | "B..DEE", 309 | "BAAF..", 310 | "..GFHH", 311 | "..GIIJ", 312 | "..G..J", 313 | }, 314 | { 315 | "BBBCDE", 316 | "FGGCDE", 317 | "F.AAD.", 318 | "HHI...", 319 | ".JI.KK", 320 | ".JLLMM", 321 | }, 322 | { 323 | "BCDDE.", 324 | "BCF.EG", 325 | "B.FAAG", 326 | "HHHI.G", 327 | "..JIKK", 328 | "LLJMM.", 329 | }, 330 | { 331 | "..BCCC", 332 | "..BDEE", 333 | "FAADGH", 334 | "FI..GH", 335 | "JIKKGL", 336 | "JMMNNL", 337 | }, 338 | { 339 | "BCCDDD", 340 | "B.EEF.", 341 | "AAG.F.", 342 | "HHGIIJ", 343 | ".KKL.J", 344 | "MMML.J", 345 | }, 346 | } 347 | -------------------------------------------------------------------------------- /cmd/generate/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "math/rand" 6 | "time" 7 | 8 | "github.com/fogleman/gg" 9 | "github.com/fogleman/rush" 10 | ) 11 | 12 | func main() { 13 | rand.Seed(time.Now().UTC().UnixNano()) 14 | 15 | generator := rush.NewDefaultGenerator() 16 | for i := 0; ; i++ { 17 | board := generator.Generate(100000) 18 | board.SortPieces() 19 | solution := board.Solve() 20 | gg.SavePNG(fmt.Sprintf("%02d-%d.png", solution.NumMoves, int(time.Now().Unix())), board.Render()) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /cmd/graph/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "os" 6 | 7 | . "github.com/fogleman/rush" 8 | ) 9 | 10 | func main() { 11 | board, err := NewBoardFromString(os.Args[1]) 12 | if err != nil { 13 | log.Fatal(err) 14 | } 15 | Graph(board) 16 | } 17 | -------------------------------------------------------------------------------- /cmd/impossible/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "sort" 6 | 7 | "github.com/fogleman/gg" 8 | "github.com/fogleman/rush" 9 | ) 10 | 11 | type Key [rush.MaxPieces]rush.Piece 12 | 13 | func makeKey(board *rush.Board) Key { 14 | pieces := make([]rush.Piece, len(board.Pieces)) 15 | copy(pieces, board.Pieces) 16 | sort.Slice(pieces, func(i, j int) bool { 17 | if i == 0 { 18 | return true 19 | } 20 | a := pieces[i] 21 | b := pieces[j] 22 | if a.Orientation != b.Orientation { 23 | return a.Orientation < b.Orientation 24 | } 25 | if a.Size != b.Size { 26 | return a.Size < b.Size 27 | } 28 | return a.Position < b.Position 29 | }) 30 | var key Key 31 | for i, piece := range pieces { 32 | key[i] = piece 33 | } 34 | return key 35 | } 36 | 37 | func main() { 38 | seen := make(map[Key]bool) 39 | counter := 0 40 | for i := 0; ; i++ { 41 | board := rush.NewRandomBoard(6, 6, 2, 2, 4, 0) 42 | if board.Impossible() { 43 | continue 44 | } 45 | key := makeKey(board) 46 | if _, ok := seen[key]; ok { 47 | continue 48 | } 49 | // fmt.Println(key) 50 | seen[key] = true 51 | if board.Validate() != nil { 52 | continue 53 | } 54 | solution := board.Solve() 55 | if solution.Solvable { 56 | continue 57 | } 58 | gg.SavePNG(fmt.Sprintf("impossible-%d.png", counter), board.Render()) 59 | counter++ 60 | fmt.Println(counter) 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /cmd/multi/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "runtime" 7 | "time" 8 | 9 | . "github.com/fogleman/rush" 10 | ) 11 | 12 | const ( 13 | W = 6 14 | H = 6 15 | 16 | PrimaryRow = 2 17 | PrimarySize = 2 18 | 19 | MinSize = 2 20 | MaxSize = 3 21 | 22 | ChannelBufferSize = 1 << 18 23 | 24 | // MaxCounter = 695 // 4x4 25 | // MaxCounter = 124886 // 5x5 26 | MaxCounter = 88914655 // 6x6 27 | ) 28 | 29 | func isCanonical(board *Board, memo *Memo, key *MemoKey, previousPiece int) bool { 30 | if board.MemoKey().Less(key, false) { 31 | return false 32 | } 33 | if !memo.Add(board.MemoKey(), 0) { 34 | return true 35 | } 36 | for _, move := range board.Moves(nil) { 37 | if move.Piece == 0 { 38 | continue 39 | } 40 | if move.Piece == previousPiece { 41 | continue 42 | } 43 | board.DoMove(move) 44 | ok := isCanonical(board, memo, key, move.Piece) 45 | board.UndoMove(move) 46 | if !ok { 47 | return false 48 | } 49 | } 50 | return true 51 | } 52 | 53 | func IsCanonical(board *Board) bool { 54 | memo := NewMemo() 55 | key := *board.MemoKey() 56 | return isCanonical(board, memo, &key, -1) 57 | } 58 | 59 | type Result struct { 60 | Board *Board 61 | Unsolved *Board 62 | Solution Solution 63 | Group int 64 | Counter uint64 65 | JobCount int 66 | CanonicalCount int 67 | NonTrivialCount int 68 | MinimalCount int 69 | Done bool 70 | } 71 | 72 | func worker(jobs <-chan EnumeratorItem, results chan<- Result) { 73 | var ( 74 | jobCount int 75 | canonicalCount int 76 | nonTrivialCount int 77 | minimalCount int 78 | ) 79 | for job := range jobs { 80 | jobCount++ 81 | 82 | board := job.Board 83 | 84 | // only evaluate "canonical" boards 85 | board.SortPieces() 86 | if !IsCanonical(board) { 87 | continue 88 | } 89 | canonicalCount++ 90 | 91 | // "unsolve" to find hardest reachable position 92 | unsolver := NewUnsolverWithStaticAnalyzer(board, nil) 93 | unsolved, solution := unsolver.UnsafeUnsolve() 94 | unsolved.SortPieces() 95 | 96 | // only interested in "non-trivial" puzzles 97 | if solution.NumMoves < 2 { 98 | continue 99 | } 100 | nonTrivialCount++ 101 | 102 | // if removing any piece does not affect the solution, skip 103 | ok := true 104 | for i := 1; i < len(unsolved.Pieces); i++ { 105 | b := unsolved.Copy() 106 | b.RemovePiece(i) 107 | s := b.UnsafeSolve() 108 | if s.NumMoves == solution.NumMoves && s.NumSteps == solution.NumSteps { 109 | ok = false 110 | break 111 | } 112 | } 113 | if !ok { 114 | continue 115 | } 116 | minimalCount++ 117 | 118 | // we are interested in this puzzle 119 | results <- Result{ 120 | board, unsolved, solution, job.Group, job.Counter, 121 | jobCount, canonicalCount, nonTrivialCount, minimalCount, false} 122 | 123 | // reset deltas 124 | jobCount = 0 125 | canonicalCount = 0 126 | nonTrivialCount = 0 127 | minimalCount = 0 128 | } 129 | results <- Result{ 130 | Done: true, 131 | JobCount: jobCount, 132 | CanonicalCount: canonicalCount, 133 | NonTrivialCount: nonTrivialCount, 134 | MinimalCount: minimalCount, 135 | } 136 | } 137 | 138 | func main() { 139 | e := NewEnumerator(W, H, PrimaryRow, PrimarySize, MinSize, MaxSize) 140 | // fmt.Println(e.Count()) 141 | // return 142 | jobs := e.Enumerate(ChannelBufferSize) 143 | results := make(chan Result, ChannelBufferSize) 144 | 145 | wn := runtime.NumCPU() 146 | for i := 0; i < wn; i++ { 147 | go worker(jobs, results) 148 | } 149 | 150 | seen := make(map[string]bool) 151 | groups := make(map[int]int) 152 | var ( 153 | jobCount int 154 | canonicalCount int 155 | nonTrivialCount int 156 | minimalCount int 157 | ) 158 | start := time.Now() 159 | for result := range results { 160 | jobCount += result.JobCount 161 | canonicalCount += result.CanonicalCount 162 | nonTrivialCount += result.NonTrivialCount 163 | minimalCount += result.MinimalCount 164 | 165 | if result.Done { 166 | wn-- 167 | if wn == 0 { 168 | break 169 | } 170 | continue 171 | } 172 | 173 | unsolved := result.Unsolved 174 | solution := result.Solution 175 | key := unsolved.Hash() 176 | if _, ok := seen[key]; ok { 177 | continue 178 | } 179 | seen[key] = true 180 | groups[result.Group]++ 181 | 182 | pct := float64(result.Counter) / MaxCounter 183 | fmt.Printf( 184 | "%02d %02d %02d %s %d %d\n", 185 | solution.NumMoves, solution.NumSteps, len(unsolved.Pieces), 186 | key, solution.MemoSize, result.Group) 187 | fmt.Fprintf( 188 | os.Stderr, "[%.9f] %d in, %d cn, %d nt, %d mn, %d dt, %d gp - %s\n", 189 | pct, jobCount, canonicalCount, nonTrivialCount, minimalCount, 190 | len(seen), len(groups), time.Since(start)) 191 | } 192 | 193 | fmt.Fprintf( 194 | os.Stderr, "[%.9f] %d in, %d cn, %d nt, %d mn, %d dt, %d gp - %s\n", 195 | 1.0, jobCount, canonicalCount, nonTrivialCount, minimalCount, 196 | len(seen), len(groups), time.Since(start)) 197 | } 198 | -------------------------------------------------------------------------------- /cmd/profile/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "net/http" 7 | _ "net/http/pprof" 8 | "time" 9 | 10 | "github.com/fogleman/rush" 11 | ) 12 | 13 | func main() { 14 | go func() { 15 | log.Println(http.ListenAndServe("localhost:6060", nil)) 16 | }() 17 | 18 | t0 := time.Now() 19 | for i := 1; ; i++ { 20 | board := rush.NewRandomBoard(6, 6, 2, 2, 10, 0) 21 | start := time.Now() 22 | solution := board.Solve() 23 | elapsed := time.Since(start) 24 | if elapsed < 100*time.Millisecond { 25 | continue 26 | } 27 | gps := float64(i) / time.Since(t0).Seconds() 28 | fmt.Printf( 29 | "%6d (%.1f): %8.6f, %5t, %2d, %d, %d\n", 30 | i, gps, elapsed.Seconds(), solution.Solvable, solution.Depth, 31 | solution.MemoSize, solution.MemoHits) 32 | if !solution.Solvable { 33 | // fmt.Println(board.Blocked()) 34 | // if board.Blocked() { 35 | // gg.SavePNG(fmt.Sprintf("blocked-%d.png", int(time.Now().Unix())), board.Render()) 36 | // } else { 37 | // gg.SavePNG(fmt.Sprintf("impossible-%d.png", int(time.Now().Unix())), board.Render()) 38 | // } 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /cmd/render/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "os" 7 | 8 | "github.com/fogleman/gg" 9 | "github.com/fogleman/rush" 10 | ) 11 | 12 | func main() { 13 | args := os.Args[1:] 14 | if len(args) != 1 && len(args) != 2 { 15 | fmt.Println("render DESC [OUTPUT]") 16 | return 17 | } 18 | 19 | board, err := rush.NewBoardFromString(args[0]) 20 | if err != nil { 21 | log.Fatal(err) 22 | } 23 | 24 | output := "out.png" 25 | if len(args) == 2 { 26 | output = args[1] 27 | } 28 | err = gg.SavePNG(output, board.Render()) 29 | if err != nil { 30 | log.Fatal(err) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /cmd/solve/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "os" 7 | 8 | "github.com/fogleman/rush" 9 | ) 10 | 11 | func main() { 12 | args := os.Args[1:] 13 | if len(args) != 1 { 14 | fmt.Println("solve DESC") 15 | return 16 | } 17 | 18 | board, err := rush.NewBoardFromString(args[0]) 19 | if err != nil { 20 | log.Fatal(err) 21 | } 22 | 23 | solution := board.Solve() 24 | fmt.Println(solution) 25 | } 26 | -------------------------------------------------------------------------------- /cmd/solver/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "os" 7 | "time" 8 | 9 | "github.com/fogleman/gg" 10 | "github.com/fogleman/rush" 11 | ) 12 | 13 | func main() { 14 | board, err := rush.NewBoardFromString(os.Args[1]) 15 | if err != nil { 16 | log.Fatal(err) 17 | } 18 | 19 | start := time.Now() 20 | solution := board.Solve() 21 | elapsed := time.Since(start) 22 | 23 | fmt.Println(solution) 24 | fmt.Println(elapsed) 25 | 26 | gg.SavePNG(fmt.Sprintf("solver-%02d.png", 0), board.Render()) 27 | for i, move := range solution.Moves { 28 | board.DoMove(move) 29 | gg.SavePNG(fmt.Sprintf("solver-%02d.png", i+1), board.Render()) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /cmd/static/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "net/http" 7 | _ "net/http/pprof" 8 | "time" 9 | 10 | . "github.com/fogleman/rush" 11 | ) 12 | 13 | const N = 100000 14 | 15 | func main() { 16 | go func() { 17 | log.Println(http.ListenAndServe("localhost:6060", nil)) 18 | }() 19 | 20 | // board, err := NewBoard([]string{ 21 | // "KGGGEE", 22 | // "K.....", 23 | // "AAC..I", 24 | // "..CFFI", 25 | // "HHCJDI", 26 | // "BBBJD.", 27 | // }) 28 | // if err != nil { 29 | // log.Fatal(err) 30 | // } 31 | 32 | start := time.Now() 33 | count := 0 34 | for i := 0; i < N; i++ { 35 | board := NewRandomBoard(6, 6, 2, 2, 10, 0) 36 | if board.Impossible() { 37 | count++ 38 | } 39 | } 40 | elapsed := time.Since(start) 41 | rate := N / elapsed.Seconds() 42 | pct := float64(count) / N 43 | fmt.Println(elapsed, rate, pct) 44 | } 45 | -------------------------------------------------------------------------------- /cmd/universe/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "sort" 6 | 7 | . "github.com/fogleman/rush" 8 | ) 9 | 10 | /* 11 | 12 | ...... 13 | 14 | ....BB 15 | ...BB. 16 | ..BB.. 17 | .BB... 18 | BB.... 19 | 20 | ...BBB 21 | ..BBB. 22 | .BBB.. 23 | BBB... 24 | 25 | ..BBCC 26 | .BB.CC 27 | .BBCC. 28 | BB..CC 29 | BB.CC. 30 | BBCC.. 31 | 32 | .BBBCC 33 | BBB.CC 34 | BBBCC. 35 | 36 | .BBCCC 37 | BB.CCC 38 | BBCCC. 39 | 40 | 6 groups: [[], [2], [3], [2, 2], [3, 2], [2, 3]] 41 | 1 primary row 42 | 5 rows 43 | 6 cols 44 | 45 | 6^11 = 362,797,056 46 | 47 | 1. precompute all possible rows/cols with bit masks 48 | 2. recursively pick rows/cols, check & mask 49 | 3. check if canonical (any position is Less - including primary!) 50 | 4. => worker unsolve+less => result 51 | 5. => check memo => write result 52 | 53 | 1. position generator routine 54 | 2. worker routines (unsolvers) 55 | 3. result handler routine 56 | 57 | Rules: 58 | 59 | A row cannot be completely filled with horizontal pieces. 60 | 61 | A column cannot be completely filled with vertical pieces. 62 | 63 | The primary row can only have one horizontal piece: the primary piece itself. 64 | 65 | The primary piece cannot start in the winning position. 66 | 67 | */ 68 | 69 | type positionEntry struct { 70 | Pieces []Piece 71 | Mask uint64 72 | Group int 73 | } 74 | 75 | func makePositionEntry(w int, pieces []Piece, groups [][]int) positionEntry { 76 | ps := make([]Piece, len(pieces)) 77 | copy(ps, pieces) 78 | var mask uint64 79 | for _, piece := range ps { 80 | idx := piece.Position 81 | stride := piece.Stride(w) 82 | for i := 0; i < piece.Size; i++ { 83 | mask |= 1 << uint(idx) 84 | idx += stride 85 | } 86 | } 87 | group := -1 88 | for i, g := range groups { 89 | if len(g) != len(pieces) { 90 | continue 91 | } 92 | ok := true 93 | for j := range g { 94 | if g[j] != pieces[j].Size { 95 | ok = false 96 | } 97 | } 98 | if ok { 99 | group = i 100 | break 101 | } 102 | } 103 | if group < 0 { 104 | panic("no group match") 105 | } 106 | return positionEntry{ps, mask, group} 107 | } 108 | 109 | type PositionGenerator struct { 110 | Width int 111 | Height int 112 | PrimaryRow int 113 | PrimarySize int 114 | MinSize int 115 | MaxSize int 116 | groups [][]int 117 | rowEntries [][]positionEntry 118 | colEntries [][]positionEntry 119 | hardestBoard *Board 120 | hardestSolution Solution 121 | counter1 uint64 122 | counter2 uint64 123 | prevGroup int 124 | } 125 | 126 | func NewPositionGenerator(w, h, pr, ps, mins, maxs int) *PositionGenerator { 127 | pg := PositionGenerator{} 128 | pg.Width = w 129 | pg.Height = h 130 | pg.PrimaryRow = pr 131 | pg.PrimarySize = ps 132 | pg.MinSize = mins 133 | pg.MaxSize = maxs 134 | pg.rowEntries = make([][]positionEntry, h) 135 | pg.colEntries = make([][]positionEntry, w) 136 | pg.precomputeGroups(nil, 0) 137 | pg.precomputePositionEntries() 138 | return &pg 139 | } 140 | 141 | func NewDefaultPositionGenerator() *PositionGenerator { 142 | return NewPositionGenerator(5, 5, 2, 2, 2, 3) 143 | } 144 | 145 | func (pg *PositionGenerator) precomputeGroups(sizes []int, sum int) { 146 | if sum >= pg.Width { 147 | return 148 | } 149 | 150 | sizesCopy := make([]int, len(sizes)) 151 | copy(sizesCopy, sizes) 152 | pg.groups = append(pg.groups, sizesCopy) 153 | 154 | n := len(sizes) 155 | for s := pg.MinSize; s <= pg.MaxSize; s++ { 156 | sizes = append(sizes, s) 157 | pg.precomputeGroups(sizes, sum+s) 158 | sizes = sizes[:n] 159 | } 160 | } 161 | 162 | func (pg *PositionGenerator) precomputeRow(y, x int, pieces []Piece) { 163 | w := pg.Width 164 | if x >= w { 165 | if y == pg.PrimaryRow { 166 | if len(pieces) != 1 { 167 | return 168 | } 169 | if pieces[0].Size != pg.PrimarySize { 170 | return 171 | } 172 | piece := pieces[0] 173 | target := (piece.Row(w)+1)*w - piece.Size 174 | if piece.Position == target { 175 | return 176 | } 177 | } 178 | var n int 179 | for _, piece := range pieces { 180 | n += piece.Size 181 | } 182 | if n >= w { 183 | return 184 | } 185 | pe := makePositionEntry(w, pieces, pg.groups) 186 | pg.rowEntries[y] = append(pg.rowEntries[y], pe) 187 | return 188 | } 189 | for s := pg.MinSize; s <= pg.MaxSize; s++ { 190 | if x+s > w { 191 | continue 192 | } 193 | p := y*w + x 194 | pieces = append(pieces, Piece{p, s, Horizontal}) 195 | pg.precomputeRow(y, x+s, pieces) 196 | pieces = pieces[:len(pieces)-1] 197 | } 198 | pg.precomputeRow(y, x+1, pieces) 199 | } 200 | 201 | func (pg *PositionGenerator) precomputeCol(x, y int, pieces []Piece) { 202 | w := pg.Width 203 | h := pg.Height 204 | if y >= h { 205 | var n int 206 | for _, piece := range pieces { 207 | n += piece.Size 208 | } 209 | if n >= h { 210 | return 211 | } 212 | pe := makePositionEntry(w, pieces, pg.groups) 213 | pg.colEntries[x] = append(pg.colEntries[x], pe) 214 | return 215 | } 216 | for s := pg.MinSize; s <= pg.MaxSize; s++ { 217 | if y+s > h { 218 | continue 219 | } 220 | p := y*w + x 221 | pieces = append(pieces, Piece{p, s, Vertical}) 222 | pg.precomputeCol(x, y+s, pieces) 223 | pieces = pieces[:len(pieces)-1] 224 | } 225 | pg.precomputeCol(x, y+1, pieces) 226 | } 227 | 228 | func (pg *PositionGenerator) precomputePositionEntries() { 229 | for y := 0; y < pg.Height; y++ { 230 | pg.precomputeRow(y, 0, nil) 231 | } 232 | for x := 0; x < pg.Width; x++ { 233 | pg.precomputeCol(x, 0, nil) 234 | } 235 | 236 | for y := 0; y < pg.Height; y++ { 237 | a := pg.rowEntries[y] 238 | sort.SliceStable(a, func(i, j int) bool { return a[i].Group < a[j].Group }) 239 | } 240 | for x := 0; x < pg.Width; x++ { 241 | a := pg.colEntries[x] 242 | sort.SliceStable(a, func(i, j int) bool { return a[i].Group < a[j].Group }) 243 | } 244 | } 245 | 246 | func (pg *PositionGenerator) populatePrimary() { 247 | var mask uint64 248 | board := NewEmptyBoard(pg.Width, pg.Height) 249 | for _, pe := range pg.rowEntries[pg.PrimaryRow] { 250 | mask |= pe.Mask 251 | for _, piece := range pe.Pieces { 252 | board.AddPiece(piece) 253 | } 254 | pg.populateRow(0, mask, 0, board) 255 | for range pe.Pieces { 256 | board.RemoveLastPiece() 257 | } 258 | mask ^= pe.Mask 259 | } 260 | } 261 | 262 | func (pg *PositionGenerator) populateRow(y int, mask uint64, group int, board *Board) { 263 | if y >= pg.Height { 264 | pg.populateCol(0, mask, group, board) 265 | return 266 | } 267 | if y == pg.PrimaryRow { 268 | pg.populateRow(y+1, mask, group, board) 269 | return 270 | } 271 | group *= len(pg.groups) 272 | for _, pe := range pg.rowEntries[y] { 273 | if mask&pe.Mask != 0 { 274 | continue 275 | } 276 | mask |= pe.Mask 277 | for _, piece := range pe.Pieces { 278 | board.AddPiece(piece) 279 | } 280 | pg.populateRow(y+1, mask, group+pe.Group, board) 281 | for range pe.Pieces { 282 | board.RemoveLastPiece() 283 | } 284 | mask ^= pe.Mask 285 | } 286 | } 287 | 288 | func (pg *PositionGenerator) populateCol(x int, mask uint64, group int, board *Board) { 289 | if x >= pg.Width { 290 | pg.counter1++ 291 | if !pg.isCanonical(board) { 292 | return 293 | } 294 | // pg.hardest(board) 295 | // if !pg.hardestSolution.Solvable { 296 | // return 297 | // } 298 | pg.counter2++ 299 | // if pg.counter1%1000000000 == 0 { 300 | // fmt.Println(pg.counter1, pg.counter2, group) 301 | // } 302 | // hardest := pg.hardestBoard 303 | // solution := pg.hardestSolution 304 | // eq := "" 305 | // if group == pg.prevGroup { 306 | // eq = "***" 307 | // } 308 | // pg.prevGroup = group 309 | // fmt.Println(pg.counter1, pg.counter2, hardest.Hash(), solution.NumMoves, solution.MemoSize, group, eq) 310 | // fmt.Println(pg.counter1, pg.counter2, board.Hash(), group, eq) 311 | return 312 | } 313 | group *= len(pg.groups) 314 | for _, pe := range pg.colEntries[x] { 315 | if mask&pe.Mask != 0 { 316 | continue 317 | } 318 | mask |= pe.Mask 319 | for _, piece := range pe.Pieces { 320 | board.AddPiece(piece) 321 | } 322 | pg.populateCol(x+1, mask, group+pe.Group, board) 323 | for range pe.Pieces { 324 | board.RemoveLastPiece() 325 | } 326 | mask ^= pe.Mask 327 | } 328 | } 329 | 330 | func (pg *PositionGenerator) canonicalSearch(board *Board, memo *Memo, key *MemoKey, previousPiece int) bool { 331 | if board.MemoKey().Less(key, true) { 332 | return false 333 | } 334 | if !memo.Add(board.MemoKey(), 0) { 335 | return true 336 | } 337 | for _, move := range board.Moves(nil) { 338 | if move.Piece == previousPiece { 339 | continue 340 | } 341 | board.DoMove(move) 342 | ok := pg.canonicalSearch(board, memo, key, move.Piece) 343 | board.UndoMove(move) 344 | if !ok { 345 | return false 346 | } 347 | } 348 | return true 349 | } 350 | 351 | func (pg *PositionGenerator) isCanonical(board *Board) bool { 352 | memo := NewMemo() 353 | key := *board.MemoKey() 354 | return pg.canonicalSearch(board, memo, &key, -1) 355 | } 356 | 357 | func (pg *PositionGenerator) hardestSearch(board *Board, memo *Memo, solver *Solver, previousPiece int) { 358 | if !memo.Add(board.MemoKey(), 0) { 359 | return 360 | } 361 | 362 | solution := solver.UnsafeSolve() 363 | delta := solution.NumMoves - pg.hardestSolution.NumMoves 364 | if delta > 0 || (delta == 0 && board.MemoKey().Less(pg.hardestBoard.MemoKey(), true)) { 365 | pg.hardestSolution = solution 366 | pg.hardestBoard = board.Copy() 367 | } 368 | 369 | for _, move := range board.Moves(nil) { 370 | if move.Piece == previousPiece { 371 | continue 372 | } 373 | board.DoMove(move) 374 | pg.hardestSearch(board, memo, solver, move.Piece) 375 | board.UndoMove(move) 376 | } 377 | } 378 | 379 | func (pg *PositionGenerator) hardest(board *Board) { 380 | memo := NewMemo() 381 | solver := NewSolver(board) 382 | pg.hardestBoard = board.Copy() 383 | pg.hardestSolution = solver.Solve() 384 | if !pg.hardestSolution.Solvable { 385 | return 386 | } 387 | pg.hardestSearch(board, memo, solver, -1) 388 | pg.hardestBoard.SortPieces() 389 | } 390 | 391 | func (pg *PositionGenerator) Generate() { 392 | pg.populatePrimary() 393 | } 394 | 395 | func main() { 396 | pg := NewDefaultPositionGenerator() 397 | pg.Generate() 398 | fmt.Println(pg.counter1, pg.counter2) 399 | // 27,103,652,326 400 | // 22,138,497,189 401 | } 402 | -------------------------------------------------------------------------------- /cmd/unsolver/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | 7 | "github.com/fogleman/rush" 8 | ) 9 | 10 | var desc = []string{ 11 | "JGCBBB", 12 | "JGC...", 13 | "....AA", 14 | "IIEKKH", 15 | "LLEF.H", 16 | ".DDF.H", 17 | } 18 | 19 | func main() { 20 | board, err := rush.NewBoard(desc) 21 | if err != nil { 22 | log.Fatal(err) 23 | } 24 | 25 | fmt.Println(board) 26 | fmt.Println() 27 | 28 | unsolver := rush.NewUnsolver(board) 29 | unsolved := unsolver.Unsolve() 30 | solution := unsolved.Solve() 31 | 32 | fmt.Println(unsolved) 33 | fmt.Println() 34 | fmt.Println(solution) 35 | } 36 | -------------------------------------------------------------------------------- /config.go: -------------------------------------------------------------------------------- 1 | package rush 2 | 3 | const MaxPieces = 18 4 | const MinPieceSize = 2 5 | const MinBoardSize = MinPieceSize + 1 6 | const MaxBoardSize = 16 7 | -------------------------------------------------------------------------------- /cpp/.gitignore: -------------------------------------------------------------------------------- 1 | bin 2 | build 3 | main 4 | -------------------------------------------------------------------------------- /cpp/Makefile: -------------------------------------------------------------------------------- 1 | #### PROJECT SETTINGS #### 2 | # The name of the executable to be created 3 | BIN_NAME := main 4 | # Compiler used 5 | C ?= g++ 6 | # Extension of source files used in the project 7 | SRC_EXT = cpp 8 | # Path to the source directory, relative to the makefile 9 | SRC_PATH = src 10 | # General compiler flags 11 | COMPILE_FLAGS = -std=c++14 -flto -O3 -Wall -Wextra -Wno-sign-compare -march=native 12 | # Additional release-specific flags 13 | RCOMPILE_FLAGS = -D NDEBUG 14 | # Additional debug-specific flags 15 | DCOMPILE_FLAGS = -D DEBUG 16 | # Add additional include paths 17 | INCLUDES = -I $(SRC_PATH) 18 | # General linker settings 19 | LINK_FLAGS = -flto -O3 20 | # Additional release-specific linker settings 21 | RLINK_FLAGS = 22 | # Additional debug-specific linker settings 23 | DLINK_FLAGS = 24 | # Destination directory, like a jail or mounted system 25 | DESTDIR = / 26 | # Install path (bin/ is appended automatically) 27 | INSTALL_PREFIX = usr/local 28 | #### END PROJECT SETTINGS #### 29 | 30 | # Generally should not need to edit below this line 31 | 32 | # Shell used in this makefile 33 | # bash is used for 'echo -en' 34 | SHELL = /bin/bash 35 | # Clear built-in rules 36 | .SUFFIXES: 37 | # Programs for installation 38 | INSTALL = install 39 | INSTALL_PROGRAM = $(INSTALL) 40 | INSTALL_DATA = $(INSTALL) -m 644 41 | 42 | # Verbose option, to output compile and link commands 43 | export V := false 44 | export CMD_PREFIX := @ 45 | ifeq ($(V),true) 46 | CMD_PREFIX := 47 | endif 48 | 49 | # Combine compiler and linker flags 50 | release: export CFLAGS := $(CFLAGS) $(COMPILE_FLAGS) $(RCOMPILE_FLAGS) 51 | release: export LDFLAGS := $(LDFLAGS) $(LINK_FLAGS) $(RLINK_FLAGS) 52 | debug: export CFLAGS := $(CFLAGS) $(COMPILE_FLAGS) $(DCOMPILE_FLAGS) 53 | debug: export LDFLAGS := $(LDFLAGS) $(LINK_FLAGS) $(DLINK_FLAGS) 54 | 55 | # Build and output paths 56 | release: export BUILD_PATH := build/release 57 | release: export BIN_PATH := bin/release 58 | debug: export BUILD_PATH := build/debug 59 | debug: export BIN_PATH := bin/debug 60 | install: export BIN_PATH := bin/release 61 | 62 | # Find all source files in the source directory, sorted by most 63 | # recently modified 64 | SOURCES = $(shell find $(SRC_PATH)/ -name '*.$(SRC_EXT)' \ 65 | | sort -k 1nr | cut -f2-) 66 | # fallback in case the above fails 67 | rwildcard = $(foreach d, $(wildcard $1*), $(call rwildcard,$d/,$2) \ 68 | $(filter $(subst *,%,$2), $d)) 69 | ifeq ($(SOURCES),) 70 | SOURCES := $(call rwildcard, $(SRC_PATH)/, *.$(SRC_EXT)) 71 | endif 72 | 73 | # Set the object file names, with the source directory stripped 74 | # from the path, and the build path prepended in its place 75 | OBJECTS = $(SOURCES:$(SRC_PATH)/%.$(SRC_EXT)=$(BUILD_PATH)/%.o) 76 | # Set the dependency files that will be used to add header dependencies 77 | DEPS = $(OBJECTS:.o=.d) 78 | 79 | # Macros for timing compilation 80 | TIME_FILE = $(dir $@).$(notdir $@)_time 81 | START_TIME = date '+%s' > $(TIME_FILE) 82 | END_TIME = read st < $(TIME_FILE) ; \ 83 | $(RM) $(TIME_FILE) ; \ 84 | st=$$((`date '+%s'` - $$st - 86400)) ; \ 85 | echo `date -u -d @$$st '+%H:%M:%S'` 86 | 87 | # Version macros 88 | # Comment/remove this section to remove versioning 89 | USE_VERSION := false 90 | # If this isn't a git repo or the repo has no tags, git describe will return non-zero 91 | ifeq ($(shell git describe > /dev/null 2>&1 ; echo $$?), 0) 92 | USE_VERSION := true 93 | VERSION := $(shell git describe --tags --long --dirty --always | \ 94 | sed 's/v\([0-9]*\)\.\([0-9]*\)\.\([0-9]*\)-\?.*-\([0-9]*\)-\(.*\)/\1 \2 \3 \4 \5/g') 95 | VERSION_MAJOR := $(word 1, $(VERSION)) 96 | VERSION_MINOR := $(word 2, $(VERSION)) 97 | VERSION_PATCH := $(word 3, $(VERSION)) 98 | VERSION_REVISION := $(word 4, $(VERSION)) 99 | VERSION_HASH := $(word 5, $(VERSION)) 100 | VERSION_STRING := \ 101 | "$(VERSION_MAJOR).$(VERSION_MINOR).$(VERSION_PATCH).$(VERSION_REVISION)-$(VERSION_HASH)" 102 | override CFLAGS := $(CFLAGS) \ 103 | -D VERSION_MAJOR=$(VERSION_MAJOR) \ 104 | -D VERSION_MINOR=$(VERSION_MINOR) \ 105 | -D VERSION_PATCH=$(VERSION_PATCH) \ 106 | -D VERSION_REVISION=$(VERSION_REVISION) \ 107 | -D VERSION_HASH=\"$(VERSION_HASH)\" 108 | endif 109 | 110 | # Standard, non-optimized release build 111 | .PHONY: release 112 | release: dirs 113 | ifeq ($(USE_VERSION), true) 114 | @echo "Beginning release build v$(VERSION_STRING)" 115 | else 116 | @echo "Beginning release build" 117 | endif 118 | @$(MAKE) all --no-print-directory 119 | 120 | # Debug build for gdb debugging 121 | .PHONY: debug 122 | debug: dirs 123 | ifeq ($(USE_VERSION), true) 124 | @echo "Beginning debug build v$(VERSION_STRING)" 125 | else 126 | @echo "Beginning debug build" 127 | endif 128 | @$(MAKE) all --no-print-directory 129 | 130 | # Create the directories used in the build 131 | .PHONY: dirs 132 | dirs: 133 | @echo "Creating directories" 134 | @mkdir -p $(dir $(OBJECTS)) 135 | @mkdir -p $(BIN_PATH) 136 | 137 | # Installs to the set path 138 | .PHONY: install 139 | install: 140 | @echo "Installing to $(DESTDIR)$(INSTALL_PREFIX)/bin" 141 | @$(INSTALL_PROGRAM) $(BIN_PATH)/$(BIN_NAME) $(DESTDIR)$(INSTALL_PREFIX)/bin 142 | 143 | # Uninstalls the program 144 | .PHONY: uninstall 145 | uninstall: 146 | @echo "Removing $(DESTDIR)$(INSTALL_PREFIX)/bin/$(BIN_NAME)" 147 | @$(RM) $(DESTDIR)$(INSTALL_PREFIX)/bin/$(BIN_NAME) 148 | 149 | # Removes all build files 150 | .PHONY: clean 151 | clean: 152 | @echo "Deleting $(BIN_NAME) symlink" 153 | @$(RM) $(BIN_NAME) 154 | @echo "Deleting directories" 155 | @$(RM) -r build 156 | @$(RM) -r bin 157 | 158 | # Main rule, checks the executable and symlinks to the output 159 | all: $(BIN_PATH)/$(BIN_NAME) 160 | @echo "Making symlink: $(BIN_NAME) -> $<" 161 | @$(RM) $(BIN_NAME) 162 | @ln -s $(BIN_PATH)/$(BIN_NAME) $(BIN_NAME) 163 | 164 | # Link the executable 165 | $(BIN_PATH)/$(BIN_NAME): $(OBJECTS) 166 | @echo "Linking: $@" 167 | $(CMD_PREFIX)$(C) $(OBJECTS) $(LDFLAGS) -o $@ 168 | 169 | # Add dependency files, if they exist 170 | -include $(DEPS) 171 | 172 | # Source file rules 173 | # After the first compilation they will be joined with the rules from the 174 | # dependency files to provide header dependencies 175 | $(BUILD_PATH)/%.o: $(SRC_PATH)/%.$(SRC_EXT) 176 | @echo "Compiling: $< -> $@" 177 | $(CMD_PREFIX)$(C) $(CFLAGS) $(INCLUDES) -MP -MMD -c $< -o $@ 178 | 179 | .PHONY: run 180 | run: release 181 | time ./$(BIN_NAME) 182 | -------------------------------------------------------------------------------- /cpp/src/bb.cpp: -------------------------------------------------------------------------------- 1 | #include "bb.h" 2 | 3 | #include "config.h" 4 | 5 | std::string BitboardString(const bb b) { 6 | std::string s(BoardSize2, '.'); 7 | for (int i = 0; i < BoardSize2; i++) { 8 | const bb mask = (bb)1 << i; 9 | if ((b & mask) != 0) { 10 | s[i] = 'X'; 11 | } 12 | } 13 | return s; 14 | } 15 | 16 | bb RandomBitboard(std::mt19937 &gen) { 17 | std::uniform_int_distribution dis(0, 0xffff); 18 | const bb a = dis(gen); 19 | const bb b = dis(gen); 20 | const bb c = dis(gen); 21 | const bb d = dis(gen); 22 | return a << 48 | b << 32 | c << 16 | d; 23 | } 24 | -------------------------------------------------------------------------------- /cpp/src/bb.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | 7 | typedef uint64_t bb; 8 | 9 | std::string BitboardString(const bb b); 10 | bb RandomBitboard(std::mt19937 &gen); 11 | -------------------------------------------------------------------------------- /cpp/src/board.cpp: -------------------------------------------------------------------------------- 1 | #include "board.h" 2 | 3 | #include 4 | #include 5 | 6 | Board::Board() : 7 | m_HorzMask(0), 8 | m_VertMask(0) 9 | { 10 | } 11 | 12 | Board::Board(std::string desc) : 13 | m_HorzMask(0), 14 | m_VertMask(0) 15 | { 16 | if (desc.length() != BoardSize2) { 17 | throw "board string is wrong length"; 18 | } 19 | 20 | std::map> positions; 21 | for (int i = 0; i < desc.length(); i++) { 22 | const char label = desc[i]; 23 | if (label == '.' || label == 'o') { 24 | continue; 25 | } 26 | positions[label].push_back(i); 27 | } 28 | 29 | std::vector labels; 30 | labels.reserve(positions.size()); 31 | for (const auto &pair : positions) { 32 | labels.push_back(pair.first); 33 | } 34 | std::sort(labels.begin(), labels.end()); 35 | 36 | m_Pieces.reserve(labels.size()); 37 | for (const char label : labels) { 38 | const auto &ps = positions[label]; 39 | if (ps.size() < MinPieceSize) { 40 | throw "piece size < MinPieceSize"; 41 | } 42 | if (ps.size() > MaxPieceSize) { 43 | throw "piece size > MaxPieceSize"; 44 | } 45 | const int stride = ps[1] - ps[0]; 46 | if (stride != H && stride != V) { 47 | throw "invalid piece shape"; 48 | } 49 | for (int i = 2; i < ps.size(); i++) { 50 | if (ps[i] - ps[i-1] != stride) { 51 | throw "invalid piece shape"; 52 | } 53 | } 54 | AddPiece(Piece(ps[0], ps.size(), stride)); 55 | } 56 | } 57 | 58 | void Board::AddPiece(const Piece &piece) { 59 | m_Pieces.push_back(piece); 60 | if (piece.Stride() == H) { 61 | m_HorzMask |= piece.Mask(); 62 | } else { 63 | m_VertMask |= piece.Mask(); 64 | } 65 | } 66 | 67 | void Board::PopPiece() { 68 | const auto &piece = m_Pieces.back(); 69 | if (piece.Stride() == H) { 70 | m_HorzMask &= ~piece.Mask(); 71 | } else { 72 | m_VertMask &= ~piece.Mask(); 73 | } 74 | m_Pieces.pop_back(); 75 | } 76 | 77 | void Board::RemovePiece(const int i) { 78 | const auto &piece = m_Pieces[i]; 79 | if (piece.Stride() == H) { 80 | m_HorzMask &= ~piece.Mask(); 81 | } else { 82 | m_VertMask &= ~piece.Mask(); 83 | } 84 | m_Pieces.erase(m_Pieces.begin() + i); 85 | } 86 | 87 | void Board::DoMove(const int i, const int steps) { 88 | auto &piece = m_Pieces[i]; 89 | if (piece.Stride() == H) { 90 | m_HorzMask &= ~piece.Mask(); 91 | piece.Move(steps); 92 | m_HorzMask |= piece.Mask(); 93 | } else { 94 | m_VertMask &= ~piece.Mask(); 95 | piece.Move(steps); 96 | m_VertMask |= piece.Mask(); 97 | } 98 | } 99 | 100 | void Board::DoMove(const Move &move) { 101 | DoMove(move.Piece(), move.Steps()); 102 | } 103 | 104 | void Board::UndoMove(const Move &move) { 105 | DoMove(move.Piece(), -move.Steps()); 106 | } 107 | 108 | void Board::Moves(std::vector &moves) const { 109 | moves.clear(); 110 | const bb boardMask = Mask(); 111 | for (int i = 0; i < m_Pieces.size(); i++) { 112 | const auto &piece = m_Pieces[i]; 113 | if (piece.Fixed()) { 114 | continue; 115 | } 116 | if (piece.Stride() == H) { 117 | // reverse / left (negative steps) 118 | if ((piece.Mask() & LeftColumn) == 0) { 119 | bb mask = (piece.Mask() >> H) & ~piece.Mask(); 120 | int steps = -1; 121 | while ((boardMask & mask) == 0) { 122 | moves.emplace_back(Move(i, steps)); 123 | if ((mask & LeftColumn) != 0) { 124 | break; 125 | } 126 | mask >>= H; 127 | steps--; 128 | } 129 | } 130 | // forward / right (positive steps) 131 | if ((piece.Mask() & RightColumn) == 0) { 132 | bb mask = (piece.Mask() << H) & ~piece.Mask(); 133 | int steps = 1; 134 | while ((boardMask & mask) == 0) { 135 | moves.emplace_back(Move(i, steps)); 136 | if ((mask & RightColumn) != 0) { 137 | break; 138 | } 139 | mask <<= H; 140 | steps++; 141 | } 142 | } 143 | } else { 144 | // reverse / up (negative steps) 145 | if ((piece.Mask() & TopRow) == 0) { 146 | bb mask = (piece.Mask() >> V) & ~piece.Mask(); 147 | int steps = -1; 148 | while ((boardMask & mask) == 0) { 149 | moves.emplace_back(Move(i, steps)); 150 | if ((mask & TopRow) != 0) { 151 | break; 152 | } 153 | mask >>= V; 154 | steps--; 155 | } 156 | } 157 | // forward / down (positive steps) 158 | if ((piece.Mask() & BottomRow) == 0) { 159 | bb mask = (piece.Mask() << V) & ~piece.Mask(); 160 | int steps = 1; 161 | while ((boardMask & mask) == 0) { 162 | moves.emplace_back(Move(i, steps)); 163 | if ((mask & BottomRow) != 0) { 164 | break; 165 | } 166 | mask <<= V; 167 | steps++; 168 | } 169 | } 170 | } 171 | } 172 | } 173 | 174 | std::string Board::String() const { 175 | std::string s(BoardSize2, '.'); 176 | for (int i = 0; i < m_Pieces.size(); i++) { 177 | const Piece &piece = m_Pieces[i]; 178 | const char c = piece.Fixed() ? 'x' : 'A' + i; 179 | int p = piece.Position(); 180 | for (int i = 0; i < piece.Size(); i++) { 181 | s[p] = c; 182 | p += piece.Stride(); 183 | } 184 | } 185 | return s; 186 | } 187 | 188 | std::string Board::String2D() const { 189 | std::string s(BoardSize * (BoardSize + 1), '.'); 190 | for (int y = 0; y < BoardSize; y++) { 191 | const int p = y * (BoardSize + 1) + BoardSize; 192 | s[p] = '\n'; 193 | } 194 | for (int i = 0; i < m_Pieces.size(); i++) { 195 | const Piece &piece = m_Pieces[i]; 196 | const char c = piece.Fixed() ? 'x' : 'A' + i; 197 | int stride = piece.Stride(); 198 | if (stride == V) { 199 | stride++; 200 | } 201 | const int y = piece.Position() / BoardSize; 202 | const int x = piece.Position() % BoardSize; 203 | int p = y * (BoardSize + 1) + x; 204 | for (int i = 0; i < piece.Size(); i++) { 205 | s[p] = c; 206 | p += stride; 207 | } 208 | } 209 | return s; 210 | } 211 | 212 | std::ostream& operator<<(std::ostream &stream, const Board &board) { 213 | return stream << board.String(); 214 | } 215 | 216 | bool operator<(const Board &b1, const Board &b2) { 217 | if (b1.HorzMask() == b2.HorzMask()) { 218 | return b1.VertMask() < b2.VertMask(); 219 | } 220 | return b1.HorzMask() < b2.HorzMask(); 221 | } 222 | -------------------------------------------------------------------------------- /cpp/src/board.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | #include "bb.h" 10 | #include "config.h" 11 | #include "move.h" 12 | #include "piece.h" 13 | 14 | typedef std::tuple BoardKey; 15 | 16 | class Board { 17 | public: 18 | Board(); 19 | explicit Board(std::string desc); 20 | 21 | bb Mask() const { 22 | return m_HorzMask | m_VertMask; 23 | } 24 | 25 | bb HorzMask() const { 26 | return m_HorzMask; 27 | } 28 | 29 | bb VertMask() const { 30 | return m_VertMask; 31 | } 32 | 33 | BoardKey Key() const { 34 | return std::make_tuple(m_HorzMask, m_VertMask); 35 | } 36 | 37 | const boost::container::small_vector &Pieces() const { 38 | return m_Pieces; 39 | } 40 | 41 | bool Solved() const { 42 | return m_Pieces[0].Position() == Target; 43 | } 44 | 45 | void AddPiece(const Piece &piece); 46 | void PopPiece(); 47 | void RemovePiece(const int i); 48 | 49 | void DoMove(const int piece, const int steps); 50 | void DoMove(const Move &move); 51 | void UndoMove(const Move &move); 52 | 53 | void Moves(std::vector &moves) const; 54 | 55 | std::string String() const; 56 | std::string String2D() const; 57 | 58 | private: 59 | bb m_HorzMask; 60 | bb m_VertMask; 61 | boost::container::small_vector m_Pieces; 62 | }; 63 | 64 | std::ostream& operator<<(std::ostream &stream, const Board &board); 65 | 66 | bool operator<(const Board &b1, const Board &b2); 67 | -------------------------------------------------------------------------------- /cpp/src/cluster.cpp: -------------------------------------------------------------------------------- 1 | #include "cluster.h" 2 | 3 | #include 4 | #include 5 | #include 6 | 7 | #include "solver.h" 8 | 9 | Cluster::Cluster(const uint64_t id, const Board &input) : 10 | m_ID(id), 11 | m_Canonical(false), 12 | m_Solvable(false), 13 | m_Minimal(false), 14 | m_NumStates(0) 15 | { 16 | // move generation buffer 17 | std::vector moves; 18 | 19 | // exploration queue 20 | std::deque queue; 21 | queue.push_back(input); 22 | 23 | // unsolve queue 24 | std::deque unsolveQueue; 25 | 26 | // large sentinel distance when distance is not yet known 27 | const int sentinel = std::numeric_limits::max(); 28 | 29 | // maps keys to distance from nearest goal state 30 | boost::unordered_map distance; 31 | distance[input.Key()] = sentinel; 32 | 33 | // explore reachable nodes 34 | while (!queue.empty()) { 35 | Board &board = queue.front(); 36 | if (board.Solved()) { 37 | m_Solvable = true; 38 | distance[board.Key()] = 0; 39 | unsolveQueue.push_back(board); 40 | } 41 | board.Moves(moves); 42 | for (const auto &move : moves) { 43 | board.DoMove(move); 44 | if (board < input) { 45 | // not canonical, exit early 46 | // and don't count non-canonical solvable boards 47 | m_Solvable = false; 48 | return; 49 | } 50 | if (distance.emplace(board.Key(), sentinel).second) { 51 | queue.push_back(board); 52 | } 53 | board.UndoMove(move); 54 | } 55 | queue.pop_front(); 56 | } 57 | 58 | m_Canonical = true; 59 | m_NumStates = distance.size(); 60 | 61 | if (!m_Solvable) { 62 | // nothing else to do if it's not solvable 63 | return; 64 | } 65 | 66 | // determine how far each state is from a goal state 67 | int maxDistance = 0; 68 | m_Unsolved = input; 69 | while (!unsolveQueue.empty()) { 70 | Board &board = unsolveQueue.front(); 71 | const int d = distance[board.Key()] + 1; 72 | board.Moves(moves); 73 | for (const auto &move : moves) { 74 | board.DoMove(move); 75 | const auto item = distance.find(board.Key()); 76 | if (item->second > d) { 77 | item->second = d; 78 | unsolveQueue.push_back(board); 79 | if (d > maxDistance) { 80 | maxDistance = d; 81 | m_Unsolved = board; 82 | } else if (d == maxDistance) { 83 | if (board < m_Unsolved) { 84 | m_Unsolved = board; 85 | } 86 | } 87 | } 88 | board.UndoMove(move); 89 | } 90 | unsolveQueue.pop_front(); 91 | } 92 | 93 | // determine if unsolved board is minimal 94 | Solver solver; 95 | const auto solution = solver.Solve(m_Unsolved); 96 | const int numPieces = input.Pieces().size(); 97 | std::vector pieceMoved(numPieces, false); 98 | for (const auto &move : solution.Moves()) { 99 | pieceMoved[move.Piece()] = true; 100 | } 101 | for (int i = 1; i < pieceMoved.size(); i++) { 102 | if (pieceMoved[i]) { 103 | continue; 104 | } 105 | Board board(m_Unsolved); 106 | board.RemovePiece(i); 107 | if (solver.CountMoves(board) == maxDistance) { 108 | return; 109 | } 110 | } 111 | m_Minimal = true; 112 | 113 | // record number of states by distance to goal 114 | m_Distances.resize(maxDistance + 1); 115 | for (const auto &item : distance) { 116 | m_Distances[item.second]++; 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /cpp/src/cluster.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | #include "board.h" 6 | 7 | class Cluster { 8 | public: 9 | Cluster(const uint64_t id, const Board &input); 10 | 11 | uint64_t ID() const { 12 | return m_ID; 13 | } 14 | 15 | bool Canonical() const { 16 | return m_Canonical; 17 | } 18 | 19 | bool Solvable() const { 20 | return m_Solvable; 21 | } 22 | 23 | bool Minimal() const { 24 | return m_Minimal; 25 | } 26 | 27 | int NumStates() const { 28 | return m_NumStates; 29 | } 30 | 31 | int NumMoves() const { 32 | return m_Distances.size() - 1; 33 | } 34 | 35 | const Board &Unsolved() const { 36 | return m_Unsolved; 37 | } 38 | 39 | const std::vector &DistanceCounts() const { 40 | return m_Distances; 41 | } 42 | 43 | private: 44 | uint64_t m_ID; 45 | bool m_Canonical; 46 | bool m_Solvable; 47 | bool m_Minimal; 48 | int m_NumStates; 49 | Board m_Unsolved; 50 | std::vector m_Distances; 51 | }; 52 | -------------------------------------------------------------------------------- /cpp/src/config.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | #include "bb.h" 6 | 7 | const int BoardSize = 5; 8 | const int PrimaryRow = 2; 9 | const int PrimarySize = 2; 10 | const int MinPieceSize = 2; 11 | const int MaxPieceSize = 3; 12 | const int MinWalls = 0; 13 | const int MaxWalls = 0; 14 | const int NumWorkers = 4; 15 | 16 | // const uint64_t MaxID = 1348; // 4x4 17 | // const uint64_t MaxID = 9803; // 4x4, 0-1 walls 18 | // const uint64_t MaxID = 33952; // 4x4, 0-2 walls 19 | // const uint64_t MaxID = 76837; // 4x4, 0-3 walls 20 | 21 | const uint64_t MaxID = 268108; // 5x5 22 | // const uint64_t MaxID = 2988669; // 5x5, 0-1 walls 23 | // const uint64_t MaxID = 16330429; // 5x5, 0-2 walls 24 | 25 | // const uint64_t MaxID = 243502785; // 6x6 26 | // const uint64_t MaxID = 3670622351; // 6x6, 0-1 walls 27 | // const uint64_t MaxID = 27403231254; // 6x6, 0-2 walls 28 | 29 | // const uint64_t MaxID = 561276504436; // 7x7 - 5h42m 30 | 31 | const int BoardSize2 = BoardSize * BoardSize; 32 | const int Target = PrimaryRow * BoardSize + BoardSize - PrimarySize; 33 | const int H = 1; // horizontal stride 34 | const int V = BoardSize; // vertical stride 35 | const bool DoWalls = MinPieceSize == 1; 36 | 37 | const std::array RowMasks = []() { 38 | std::array rowMasks; 39 | for (int y = 0; y < BoardSize; y++) { 40 | bb mask = 0; 41 | for (int x = 0; x < BoardSize; x++) { 42 | const int i = y * BoardSize + x; 43 | mask |= (bb)1 << i; 44 | } 45 | rowMasks[y] = mask; 46 | } 47 | return rowMasks; 48 | }(); 49 | 50 | const std::array ColumnMasks = []() { 51 | std::array columnMasks; 52 | for (int x = 0; x < BoardSize; x++) { 53 | bb mask = 0; 54 | for (int y = 0; y < BoardSize; y++) { 55 | const int i = y * BoardSize + x; 56 | mask |= (bb)1 << i; 57 | } 58 | columnMasks[x] = mask; 59 | } 60 | return columnMasks; 61 | }(); 62 | 63 | const bb TopRow = RowMasks.front(); 64 | const bb BottomRow = RowMasks.back(); 65 | const bb LeftColumn = ColumnMasks.front(); 66 | const bb RightColumn = ColumnMasks.back(); 67 | -------------------------------------------------------------------------------- /cpp/src/enumerator.cpp: -------------------------------------------------------------------------------- 1 | #include "enumerator.h" 2 | 3 | #include 4 | #include 5 | 6 | #include "config.h" 7 | 8 | PositionEntry::PositionEntry(const int group, const std::vector &pieces) : 9 | m_Group(group), 10 | m_Pieces(pieces), 11 | m_Mask(0), 12 | m_Require(0) 13 | { 14 | bb movableMask = 0; 15 | for (const auto &piece : pieces) { 16 | m_Mask |= piece.Mask(); 17 | if (!piece.Fixed()) { 18 | movableMask |= piece.Mask(); 19 | } 20 | } 21 | if (!pieces.empty()) { 22 | const int stride = pieces[0].Stride(); 23 | if (stride == H) { 24 | m_Require = (movableMask >> stride) & ~m_Mask & ~RightColumn; 25 | } else { 26 | m_Require = (movableMask >> stride) & ~m_Mask; 27 | } 28 | } 29 | } 30 | 31 | Enumerator::Enumerator() { 32 | std::vector sizes; 33 | ComputeGroups(sizes, 0); 34 | ComputePositionEntries(); 35 | } 36 | 37 | void Enumerator::Enumerate(EnumeratorFunc func) { 38 | Board board; 39 | uint64_t id = 0; 40 | PopulatePrimaryRow(func, board, id); 41 | } 42 | 43 | void Enumerator::PopulatePrimaryRow( 44 | EnumeratorFunc func, Board &board, uint64_t &id) const 45 | { 46 | for (const auto &pe : m_RowEntries[PrimaryRow]) { 47 | for (const auto &piece : pe.Pieces()) { 48 | board.AddPiece(piece); 49 | } 50 | PopulateRow(func, board, id, 0, pe.Mask(), pe.Require()); 51 | for (int i = 0; i < pe.Pieces().size(); i++) { 52 | board.PopPiece(); 53 | } 54 | } 55 | } 56 | 57 | void Enumerator::PopulateRow( 58 | EnumeratorFunc func, Board &board, uint64_t &id, int y, 59 | bb mask, bb require) const 60 | { 61 | if (DoWalls) { 62 | int walls = 0; 63 | for (const auto &piece : board.Pieces()) { 64 | if (piece.Fixed()) { 65 | walls++; 66 | } 67 | } 68 | if (walls > MaxWalls) { 69 | return; 70 | } 71 | if (y >= BoardSize && walls < MinWalls) { 72 | return; 73 | } 74 | } 75 | if (y >= BoardSize) { 76 | PopulateColumn(func, board, id, 0, mask, require); 77 | return; 78 | } 79 | if (y == PrimaryRow) { 80 | PopulateRow(func, board, id, y + 1, mask, require); 81 | return; 82 | } 83 | for (const auto &pe : m_RowEntries[y]) { 84 | if ((mask & pe.Mask()) != 0) { 85 | continue; 86 | } 87 | for (const auto &piece : pe.Pieces()) { 88 | board.AddPiece(piece); 89 | } 90 | PopulateRow( 91 | func, board, id, y + 1, 92 | mask | pe.Mask(), require | pe.Require()); 93 | for (int i = 0; i < pe.Pieces().size(); i++) { 94 | board.PopPiece(); 95 | } 96 | } 97 | } 98 | 99 | void Enumerator::PopulateColumn( 100 | EnumeratorFunc func, Board &board, uint64_t &id, int x, 101 | bb mask, bb require) const 102 | { 103 | if (x >= BoardSize) { 104 | func(id, board); 105 | id++; 106 | return; 107 | } 108 | for (const auto &pe : m_ColumnEntries[x]) { 109 | if ((mask & pe.Mask()) != 0) { 110 | continue; 111 | } 112 | if ((mask & pe.Require()) != pe.Require()) { 113 | continue; 114 | } 115 | const bb columnRequire = require & ColumnMasks[x]; 116 | if ((pe.Mask() & columnRequire) != columnRequire) { 117 | continue; 118 | } 119 | for (const auto &piece : pe.Pieces()) { 120 | board.AddPiece(piece); 121 | } 122 | PopulateColumn( 123 | func, board, id, x + 1, 124 | mask | pe.Mask(), require | pe.Require()); 125 | for (int i = 0; i < pe.Pieces().size(); i++) { 126 | board.PopPiece(); 127 | } 128 | } 129 | } 130 | 131 | void Enumerator::ComputeGroups(std::vector &sizes, int sum) { 132 | if (sum >= BoardSize) { 133 | return; 134 | } 135 | int walls = 0; 136 | for (const int size : sizes) { 137 | if (size == 1) { 138 | walls++; 139 | } 140 | } 141 | if (walls > MaxWalls) { 142 | return; 143 | } 144 | m_Groups.push_back(sizes); 145 | for (int s = MinPieceSize; s <= MaxPieceSize; s++) { 146 | sizes.push_back(s); 147 | ComputeGroups(sizes, sum + s); 148 | sizes.pop_back(); 149 | } 150 | } 151 | 152 | int Enumerator::GroupForPieces(const std::vector &pieces) { 153 | for (int i = 0; i < m_Groups.size(); i++) { 154 | const auto &group = m_Groups[i]; 155 | if (group.size() != pieces.size()) { 156 | continue; 157 | } 158 | bool ok = true; 159 | for (int j = 0; j < group.size(); j++) { 160 | if (group[j] != pieces[j].Size()) { 161 | ok = false; 162 | break; 163 | } 164 | } 165 | if (ok) { 166 | return i; 167 | } 168 | } 169 | throw "GroupForPieces failed"; 170 | } 171 | 172 | void Enumerator::ComputeRow(int y, int x, std::vector &pieces) { 173 | if (x >= BoardSize) { 174 | int n = 0; 175 | int walls = 0; 176 | for (const auto &piece : pieces) { 177 | n += piece.Size(); 178 | if (piece.Fixed()) { 179 | walls++; 180 | } 181 | } 182 | if (walls > MaxWalls) { 183 | return; 184 | } 185 | if (n >= BoardSize) { 186 | return; 187 | } 188 | std::vector ps = pieces; 189 | // special constraints for the primary row 190 | if (y == PrimaryRow) { 191 | // can only have one non-wall (the primary piece itself) 192 | const int nonWalls = ps.size() - walls; 193 | if (nonWalls != 1) { 194 | return; 195 | } 196 | // find the non-wall 197 | int primaryIndex = -1; 198 | for (int i = 0; i < ps.size(); i++) { 199 | if (!ps[i].Fixed()) { 200 | primaryIndex = i; 201 | break; 202 | } 203 | } 204 | if (primaryIndex < 0) { 205 | return; 206 | } 207 | // swap it to position zero 208 | std::swap(ps[0], ps[primaryIndex]); 209 | // check its size 210 | if (ps[0].Size() != PrimarySize) { 211 | return; 212 | } 213 | // no walls can appear to the right of the primary piece 214 | for (int i = 1; i < ps.size(); i++) { 215 | if (ps[i].Position() > ps[0].Position()) { 216 | return; 217 | } 218 | } 219 | } 220 | const int group = GroupForPieces(ps); 221 | m_RowEntries[y].emplace_back(PositionEntry(group, ps)); 222 | return; 223 | } 224 | for (int s = MinPieceSize; s <= MaxPieceSize; s++) { 225 | if (x + s > BoardSize) { 226 | continue; 227 | } 228 | const int p = y * BoardSize + x; 229 | pieces.emplace_back(Piece(p, s, H)); 230 | ComputeRow(y, x + s, pieces); 231 | pieces.pop_back(); 232 | } 233 | ComputeRow(y, x + 1, pieces); 234 | } 235 | 236 | void Enumerator::ComputeColumn(int x, int y, std::vector &pieces) { 237 | if (y >= BoardSize) { 238 | int n = 0; 239 | for (const auto &piece : pieces) { 240 | n += piece.Size(); 241 | } 242 | if (n >= BoardSize) { 243 | return; 244 | } 245 | const int group = GroupForPieces(pieces); 246 | m_ColumnEntries[x].emplace_back(PositionEntry(group, pieces)); 247 | return; 248 | } 249 | for (int s = MinPieceSize; s <= MaxPieceSize; s++) { 250 | if (s == 1) { 251 | // no "vertical" walls 252 | continue; 253 | } 254 | if (y + s > BoardSize) { 255 | continue; 256 | } 257 | const int p = y * BoardSize + x; 258 | pieces.emplace_back(Piece(p, s, V)); 259 | ComputeColumn(x, y + s, pieces); 260 | pieces.pop_back(); 261 | } 262 | ComputeColumn(x, y + 1, pieces); 263 | } 264 | 265 | void Enumerator::ComputePositionEntries() { 266 | m_RowEntries.resize(BoardSize); 267 | m_ColumnEntries.resize(BoardSize); 268 | std::vector pieces; 269 | for (int i = 0; i < BoardSize; i++) { 270 | ComputeRow(i, 0, pieces); 271 | ComputeColumn(i, 0, pieces); 272 | } 273 | for (int i = 0; i < BoardSize; i++) { 274 | std::stable_sort(m_RowEntries[i].begin(), m_RowEntries[i].end(), 275 | [](const PositionEntry &a, const PositionEntry &b) 276 | { 277 | return a.Group() < b.Group(); 278 | }); 279 | std::stable_sort(m_ColumnEntries[i].begin(), m_ColumnEntries[i].end(), 280 | [](const PositionEntry &a, const PositionEntry &b) 281 | { 282 | return a.Group() < b.Group(); 283 | }); 284 | } 285 | } 286 | -------------------------------------------------------------------------------- /cpp/src/enumerator.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | #include "bb.h" 7 | #include "board.h" 8 | #include "piece.h" 9 | 10 | class PositionEntry { 11 | public: 12 | PositionEntry(const int group, const std::vector &pieces); 13 | 14 | int Group() const { 15 | return m_Group; 16 | } 17 | 18 | const std::vector &Pieces() const { 19 | return m_Pieces; 20 | } 21 | 22 | bb Mask() const { 23 | return m_Mask; 24 | } 25 | 26 | bb Require() const { 27 | return m_Require; 28 | } 29 | 30 | private: 31 | int m_Group; 32 | std::vector m_Pieces; 33 | bb m_Mask; 34 | bb m_Require; 35 | }; 36 | 37 | typedef std::function EnumeratorFunc; 38 | 39 | class Enumerator { 40 | public: 41 | Enumerator(); 42 | void Enumerate(EnumeratorFunc func); 43 | 44 | private: 45 | void PopulatePrimaryRow( 46 | EnumeratorFunc func, Board &board, uint64_t &id) const; 47 | void PopulateRow( 48 | EnumeratorFunc func, Board &board, uint64_t &id, int y, 49 | bb mask, bb require) const; 50 | void PopulateColumn( 51 | EnumeratorFunc func, Board &board, uint64_t &id, int x, 52 | bb mask, bb require) const; 53 | 54 | void ComputeGroups(std::vector &sizes, int sum); 55 | int GroupForPieces(const std::vector &pieces); 56 | 57 | void ComputeRow(int y, int x, std::vector &pieces); 58 | void ComputeColumn(int x, int y, std::vector &pieces); 59 | void ComputePositionEntries(); 60 | 61 | std::vector> m_Groups; 62 | std::vector> m_RowEntries; 63 | std::vector> m_ColumnEntries; 64 | }; 65 | -------------------------------------------------------------------------------- /cpp/src/main.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | #include "board.h" 9 | #include "cluster.h" 10 | #include "config.h" 11 | #include "enumerator.h" 12 | #include "solver.h" 13 | 14 | using namespace std; 15 | using namespace std::chrono; 16 | 17 | typedef std::function CallbackFunc; 18 | 19 | void worker(const int wi, const int wn, CallbackFunc func) { 20 | Enumerator enumerator; 21 | enumerator.Enumerate([&](uint64_t id, const Board &board) { 22 | if (id % wn != wi) { 23 | return; 24 | } 25 | Cluster cluster(id, board); 26 | func(cluster); 27 | }); 28 | } 29 | 30 | int main() { 31 | // uint64_t lastID = 0; 32 | // Enumerator enumerator; 33 | // enumerator.Enumerate([&](uint64_t id, const Board &board) { 34 | // lastID = std::max(lastID, id); 35 | // }); 36 | // cout << lastID << endl; 37 | // return 0; 38 | 39 | mutex m; 40 | 41 | uint64_t maxSeenID = 0; 42 | uint64_t numIn = 0; 43 | uint64_t numCanonical = 0; 44 | uint64_t numSolvable = 0; 45 | uint64_t numMinimal = 0; 46 | 47 | auto start = steady_clock::now(); 48 | 49 | auto callback = [&](const Cluster &c) { 50 | lock_guard lock(m); 51 | 52 | numIn++; 53 | if (c.Canonical()) numCanonical++; 54 | if (c.Solvable()) numSolvable++; 55 | if (c.Minimal()) numMinimal++; 56 | if (!c.Canonical() || !c.Solvable() || !c.Minimal()) { 57 | return; 58 | } 59 | 60 | maxSeenID = std::max(maxSeenID, c.ID()); 61 | const Board &unsolved = c.Unsolved(); 62 | const double pct = (double)maxSeenID / (double)MaxID; 63 | const double hrs = duration(steady_clock::now() - start).count() / 3600; 64 | const double est = pct > 0 ? hrs / pct : 0; 65 | 66 | // print results to stdout 67 | cout 68 | << setfill('0') 69 | << setw(2) << c.NumMoves() << " " 70 | << unsolved << " " 71 | << c.NumStates() << " "; 72 | for (int i = 0; i < c.DistanceCounts().size(); i++) { 73 | if (i != 0) { 74 | cout << ","; 75 | } 76 | cout << c.DistanceCounts()[i]; 77 | } 78 | cout << endl; 79 | 80 | // print progress info to stderr 81 | cerr 82 | << fixed 83 | << pct << " pct " 84 | << hrs << " hrs " 85 | << est << " est - " 86 | << numIn << " inp " 87 | << numCanonical << " can " 88 | << numSolvable << " slv " 89 | << numMinimal << " min" 90 | << endl; 91 | }; 92 | 93 | std::vector threads; 94 | const int wn = NumWorkers; 95 | for (int wi = 0; wi < wn; wi++) { 96 | threads.push_back(std::thread(worker, wi, wn, callback)); 97 | } 98 | for (int wi = 0; wi < wn; wi++) { 99 | threads[wi].join(); 100 | } 101 | 102 | // print final stats to stderr 103 | const double pct = (double)maxSeenID / (double)MaxID; 104 | const double hrs = duration(steady_clock::now() - start).count() / 3600; 105 | const double est = pct > 0 ? hrs / pct : 0; 106 | cerr 107 | << fixed 108 | << 1.0 << " pct " 109 | << hrs << " hrs " 110 | << est << " est - " 111 | << numIn << " inp " 112 | << numCanonical << " can " 113 | << numSolvable << " slv " 114 | << numMinimal << " min" 115 | << endl; 116 | return 0; 117 | } 118 | 119 | int main2() { 120 | // // 51 83 13 BCDDE.BCF.EGB.FAAGHHHI.G..JIKKLLJMM. 4780 121 | Board board("BCDDE.BCF.EGB.FAAGHHHI.G..JIKKLLJMM."); 122 | 123 | Solver solver; 124 | for (int i = 0; i < 100; i++) { 125 | solver.Solve(board); 126 | } 127 | // Solver solver(board); 128 | // const int numMoves = solver.Solve(); 129 | // cout << numMoves << endl; 130 | 131 | // // 15 32 12 BB.C...D.CEE.DAAFGH.IIFGH.JKK.LLJ... 541934 132 | // Board board("BB.C...D.CEE.DAAFGH.IIFGH.JKK.LLJ..."); 133 | 134 | // // 24 43 13 B..CDDBEEC.F.G.AAF.GHHIJKKL.IJ..L.MM 278666 135 | // // Board board("B..CDDBEEC.F.G.AAF.GHHIJKKL.IJ..L.MM"); 136 | 137 | // Cluster cluster(board); 138 | 139 | // cout << "canonical: " << cluster.Canonical() << endl; 140 | // cout << "solvable: " << cluster.Solvable() << endl; 141 | // cout << "states: " << cluster.NumStates() << endl; 142 | // cout << "moves: " << cluster.NumMoves() << endl; 143 | // cout << "counts: "; 144 | 145 | // for (int count : cluster.DistanceCounts()) { 146 | // cout << count << ","; 147 | // } 148 | // cout << endl; 149 | // cout << endl; 150 | 151 | // cout << "unsolved:" << endl; 152 | // cout << cluster.Unsolved().String2D() << endl; 153 | // cout << "solved:" << endl; 154 | // cout << cluster.Solved().String2D() << endl; 155 | 156 | return 0; 157 | } 158 | -------------------------------------------------------------------------------- /cpp/src/move.cpp: -------------------------------------------------------------------------------- 1 | #include "move.h" 2 | 3 | Move::Move(int piece, int steps) : 4 | m_Piece(piece), 5 | m_Steps(steps) 6 | {} 7 | -------------------------------------------------------------------------------- /cpp/src/move.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | class Move { 4 | public: 5 | Move() = default; 6 | explicit Move(int piece, int steps); 7 | 8 | int Piece() const { 9 | return m_Piece; 10 | } 11 | 12 | int Steps() const { 13 | return m_Steps; 14 | } 15 | 16 | private: 17 | int m_Piece; 18 | int m_Steps; 19 | }; 20 | -------------------------------------------------------------------------------- /cpp/src/piece.cpp: -------------------------------------------------------------------------------- 1 | #include "piece.h" 2 | 3 | Piece::Piece(int position, int size, int stride) : 4 | m_Position(position), 5 | m_Size(size), 6 | m_Stride(stride), 7 | m_Mask(0) 8 | { 9 | int p = position; 10 | for (int i = 0; i < size; i++) { 11 | m_Mask |= (bb)1 << p; 12 | p += stride; 13 | } 14 | } 15 | 16 | void Piece::Move(int steps) { 17 | const int d = m_Stride * steps; 18 | m_Position += d; 19 | if (steps > 0) { 20 | m_Mask <<= d; 21 | } else { 22 | m_Mask >>= -d; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /cpp/src/piece.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "bb.h" 4 | #include "config.h" 5 | 6 | class Piece { 7 | public: 8 | explicit Piece(int position, int size, int stride); 9 | 10 | int Position() const { 11 | return m_Position; 12 | } 13 | 14 | int Size() const { 15 | return m_Size; 16 | } 17 | 18 | int Stride() const { 19 | return m_Stride; 20 | } 21 | 22 | bb Mask() const { 23 | return m_Mask; 24 | } 25 | 26 | bool Fixed() const { 27 | return m_Size == 1; 28 | } 29 | 30 | void Move(int steps); 31 | 32 | private: 33 | int m_Position; 34 | int m_Size; 35 | int m_Stride; 36 | bb m_Mask; 37 | }; 38 | -------------------------------------------------------------------------------- /cpp/src/solver.cpp: -------------------------------------------------------------------------------- 1 | #include "solver.h" 2 | 3 | Solution::Solution(const std::vector &moves) : 4 | m_Moves(moves) 5 | { 6 | } 7 | 8 | Solution Solver::Solve(Board &board) { 9 | m_Moves.resize(0); 10 | if (board.Solved()) { 11 | return Solution(m_Moves); 12 | } 13 | m_Memo.clear(); 14 | for (int i = 1; ; i++) { 15 | m_Moves.resize(i); 16 | m_MoveBuffers.resize(i); 17 | if (Search(board, 0, i, -1)) { 18 | return Solution(m_Moves); 19 | } 20 | } 21 | } 22 | 23 | int Solver::CountMoves(Board &board) { 24 | if (board.Solved()) { 25 | return 0; 26 | } 27 | m_Memo.clear(); 28 | for (int i = 1; ; i++) { 29 | m_Moves.resize(i); 30 | m_MoveBuffers.resize(i); 31 | if (Search(board, 0, i, -1)) { 32 | return i; 33 | } 34 | } 35 | } 36 | 37 | bool Solver::Search(Board &board, int depth, int maxDepth, int previousPiece) { 38 | int height = maxDepth - depth; 39 | if (height == 0) { 40 | return board.Solved(); 41 | } 42 | 43 | const auto item = m_Memo.find(board.Key()); 44 | if (item != m_Memo.end() && item->second >= height) { 45 | return false; 46 | } 47 | m_Memo[board.Key()] = height; 48 | 49 | // count occupied squares between primary piece and target 50 | const bb boardMask = board.Mask(); 51 | const auto &primary = board.Pieces()[0]; 52 | const int i0 = primary.Position() + primary.Size(); 53 | const int i1 = Target + primary.Size() - 1; 54 | int minMoves = 0; 55 | for (int i = i0; i <= i1; i++) { 56 | const bb mask = (bb)1 << i; 57 | if ((mask & boardMask) != 0) { 58 | minMoves++; 59 | } 60 | } 61 | if (minMoves >= height) { 62 | return false; 63 | } 64 | 65 | auto &moves = m_MoveBuffers[depth]; 66 | board.Moves(moves); 67 | for (const auto &move : moves) { 68 | if (move.Piece() == previousPiece) { 69 | continue; 70 | } 71 | board.DoMove(move); 72 | bool solved = Search(board, depth + 1, maxDepth, move.Piece()); 73 | board.UndoMove(move); 74 | if (solved) { 75 | m_Memo[board.Key()] = height - 1; 76 | m_Moves[depth] = move; 77 | return true; 78 | } 79 | } 80 | 81 | return false; 82 | } 83 | -------------------------------------------------------------------------------- /cpp/src/solver.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | #include "board.h" 7 | 8 | class Solution { 9 | public: 10 | explicit Solution(const std::vector &moves); 11 | 12 | const std::vector &Moves() const { 13 | return m_Moves; 14 | } 15 | 16 | int NumMoves() const { 17 | return m_Moves.size(); 18 | } 19 | 20 | private: 21 | std::vector m_Moves; 22 | }; 23 | 24 | class Solver { 25 | public: 26 | Solution Solve(Board &board); 27 | int CountMoves(Board &board); 28 | private: 29 | bool Search(Board &board, int depth, int maxDepth, int previousPiece); 30 | std::vector m_Moves; 31 | std::vector> m_MoveBuffers; 32 | boost::unordered_map m_Memo; 33 | }; 34 | -------------------------------------------------------------------------------- /enumerator.go: -------------------------------------------------------------------------------- 1 | package rush 2 | 3 | import ( 4 | "math" 5 | "sort" 6 | ) 7 | 8 | type positionEntry struct { 9 | Pieces []Piece 10 | Mask uint64 11 | Require uint64 12 | Group int 13 | } 14 | 15 | func makePositionEntry(stride int, noRequire uint64, pieces []Piece, groups [][]int) positionEntry { 16 | ps := make([]Piece, len(pieces)) 17 | copy(ps, pieces) 18 | var mask uint64 19 | for _, piece := range ps { 20 | idx := piece.Position 21 | for i := 0; i < piece.Size; i++ { 22 | mask |= 1 << uint(idx) 23 | idx += stride 24 | } 25 | } 26 | group := -1 27 | for i, g := range groups { 28 | if len(g) != len(pieces) { 29 | continue 30 | } 31 | ok := true 32 | for j := range g { 33 | if g[j] != pieces[j].Size { 34 | ok = false 35 | break 36 | } 37 | } 38 | if ok { 39 | group = i 40 | break 41 | } 42 | } 43 | if group < 0 { 44 | panic("makePositionEntry failed") 45 | } 46 | require := (mask >> uint(stride)) & ^mask & ^noRequire 47 | return positionEntry{ps, mask, require, group} 48 | } 49 | 50 | type EnumeratorItem struct { 51 | Board *Board 52 | Group int 53 | Counter uint64 54 | } 55 | 56 | type Enumerator struct { 57 | width int 58 | height int 59 | primaryRow int 60 | primarySize int 61 | minSize int 62 | maxSize int 63 | noRequire uint64 64 | groups [][]int 65 | rowEntries [][]positionEntry 66 | colEntries [][]positionEntry 67 | } 68 | 69 | func NewEnumerator(w, h, pr, ps, mins, maxs int) *Enumerator { 70 | e := Enumerator{} 71 | e.width = w 72 | e.height = h 73 | e.primaryRow = pr 74 | e.primarySize = ps 75 | e.minSize = mins 76 | e.maxSize = maxs 77 | e.rowEntries = make([][]positionEntry, h) 78 | e.colEntries = make([][]positionEntry, w) 79 | for y := 0; y <= h; y++ { 80 | e.noRequire |= 1 << uint(y*w+w-1) 81 | } 82 | e.precomputeGroups(nil, 0) 83 | e.precomputePositionEntries() 84 | return &e 85 | } 86 | 87 | func NewDefaultEnumerator() *Enumerator { 88 | return NewEnumerator(6, 6, 2, 2, 2, 3) 89 | } 90 | 91 | func (e *Enumerator) Enumerate(channelBufferSize int) <-chan EnumeratorItem { 92 | ch := make(chan EnumeratorItem, channelBufferSize) 93 | go func() { 94 | e.populatePrimaryRow(ch) 95 | close(ch) 96 | }() 97 | return ch 98 | } 99 | 100 | func (e *Enumerator) MaxGroup() int { 101 | n := e.width + e.height - 1 102 | return int(math.Pow(float64(len(e.groups)), float64(n))) 103 | } 104 | 105 | func (e *Enumerator) Count() uint64 { 106 | // 4x4 = 2896 107 | // 5x5 = 1566424 108 | // 6x6 = 4965155137 109 | 110 | // with require mask: 111 | // 4x4 = 695 112 | // 5x5 = 124886 113 | // 6x6 = 88914655 114 | return e.countPrimaryRow() 115 | } 116 | 117 | func (e *Enumerator) precomputeGroups(sizes []int, sum int) { 118 | if sum >= e.width { 119 | return 120 | } 121 | 122 | sizesCopy := make([]int, len(sizes)) 123 | copy(sizesCopy, sizes) 124 | e.groups = append(e.groups, sizesCopy) 125 | 126 | n := len(sizes) 127 | for s := e.minSize; s <= e.maxSize; s++ { 128 | sizes = append(sizes, s) 129 | e.precomputeGroups(sizes, sum+s) 130 | sizes = sizes[:n] 131 | } 132 | } 133 | 134 | func (e *Enumerator) precomputeRow(y, x int, pieces []Piece) { 135 | w := e.width 136 | if x >= w { 137 | if y == e.primaryRow { 138 | if len(pieces) != 1 { 139 | return 140 | } 141 | if pieces[0].Size != e.primarySize { 142 | return 143 | } 144 | piece := pieces[0] 145 | target := (piece.Row(w)+1)*w - piece.Size 146 | if piece.Position != target { 147 | return 148 | } 149 | } 150 | var n int 151 | for _, piece := range pieces { 152 | n += piece.Size 153 | } 154 | if n >= w { 155 | return 156 | } 157 | pe := makePositionEntry(1, e.noRequire, pieces, e.groups) 158 | e.rowEntries[y] = append(e.rowEntries[y], pe) 159 | return 160 | } 161 | for s := e.minSize; s <= e.maxSize; s++ { 162 | if x+s > w { 163 | continue 164 | } 165 | p := y*w + x 166 | pieces = append(pieces, Piece{p, s, Horizontal}) 167 | e.precomputeRow(y, x+s, pieces) 168 | pieces = pieces[:len(pieces)-1] 169 | } 170 | e.precomputeRow(y, x+1, pieces) 171 | } 172 | 173 | func (e *Enumerator) precomputeCol(x, y int, pieces []Piece) { 174 | w := e.width 175 | h := e.height 176 | if y >= h { 177 | var n int 178 | for _, piece := range pieces { 179 | n += piece.Size 180 | } 181 | if n >= h { 182 | return 183 | } 184 | pe := makePositionEntry(w, 0, pieces, e.groups) 185 | e.colEntries[x] = append(e.colEntries[x], pe) 186 | return 187 | } 188 | for s := e.minSize; s <= e.maxSize; s++ { 189 | if y+s > h { 190 | continue 191 | } 192 | p := y*w + x 193 | pieces = append(pieces, Piece{p, s, Vertical}) 194 | e.precomputeCol(x, y+s, pieces) 195 | pieces = pieces[:len(pieces)-1] 196 | } 197 | e.precomputeCol(x, y+1, pieces) 198 | } 199 | 200 | func (e *Enumerator) precomputePositionEntries() { 201 | for y := 0; y < e.height; y++ { 202 | e.precomputeRow(y, 0, nil) 203 | } 204 | for x := 0; x < e.width; x++ { 205 | e.precomputeCol(x, 0, nil) 206 | } 207 | 208 | for y := 0; y < e.height; y++ { 209 | a := e.rowEntries[y] 210 | sort.SliceStable(a, func(i, j int) bool { return a[i].Group < a[j].Group }) 211 | } 212 | for x := 0; x < e.width; x++ { 213 | a := e.colEntries[x] 214 | sort.SliceStable(a, func(i, j int) bool { return a[i].Group < a[j].Group }) 215 | } 216 | } 217 | 218 | func (e *Enumerator) populatePrimaryRow(ch chan EnumeratorItem) { 219 | var counter uint64 220 | board := NewEmptyBoard(e.width, e.height) 221 | for _, pe := range e.rowEntries[e.primaryRow] { 222 | for _, piece := range pe.Pieces { 223 | board.addPiece(piece) 224 | } 225 | e.populateRow(ch, &counter, 0, pe.Mask, 0, 0, board) 226 | for range pe.Pieces { 227 | board.RemoveLastPiece() 228 | } 229 | } 230 | } 231 | 232 | func (e *Enumerator) populateRow(ch chan EnumeratorItem, counter *uint64, y int, mask, require uint64, group int, board *Board) { 233 | if y >= e.height { 234 | e.populateCol(ch, counter, 0, mask, require, group, board) 235 | return 236 | } 237 | if y == e.primaryRow { 238 | e.populateRow(ch, counter, y+1, mask, require, group, board) 239 | return 240 | } 241 | group *= len(e.groups) 242 | for _, pe := range e.rowEntries[y] { 243 | if mask&pe.Mask != 0 { 244 | continue 245 | } 246 | for _, piece := range pe.Pieces { 247 | board.addPiece(piece) 248 | } 249 | e.populateRow(ch, counter, y+1, mask|pe.Mask, require|pe.Require, group+pe.Group, board) 250 | for range pe.Pieces { 251 | board.RemoveLastPiece() 252 | } 253 | } 254 | } 255 | 256 | func (e *Enumerator) populateCol(ch chan EnumeratorItem, counter *uint64, x int, mask, require uint64, group int, board *Board) { 257 | if x >= e.width { 258 | if mask&require != require { 259 | return 260 | } 261 | *counter++ 262 | ch <- EnumeratorItem{board.Copy(), group, *counter} 263 | return 264 | } 265 | group *= len(e.groups) 266 | for _, pe := range e.colEntries[x] { 267 | if mask&pe.Mask != 0 { 268 | continue 269 | } 270 | for _, piece := range pe.Pieces { 271 | board.addPiece(piece) 272 | } 273 | e.populateCol(ch, counter, x+1, mask|pe.Mask, require|pe.Require, group+pe.Group, board) 274 | for range pe.Pieces { 275 | board.RemoveLastPiece() 276 | } 277 | } 278 | } 279 | 280 | func (e *Enumerator) countPrimaryRow() uint64 { 281 | var counter uint64 282 | for _, pe := range e.rowEntries[e.primaryRow] { 283 | e.countRow(0, pe.Mask, 0, &counter) 284 | } 285 | return counter 286 | } 287 | 288 | func (e *Enumerator) countRow(y int, mask, require uint64, counter *uint64) { 289 | if y >= e.height { 290 | e.countCol(0, mask, require, counter) 291 | return 292 | } 293 | if y == e.primaryRow { 294 | e.countRow(y+1, mask, require, counter) 295 | return 296 | } 297 | for _, pe := range e.rowEntries[y] { 298 | if mask&pe.Mask != 0 { 299 | continue 300 | } 301 | e.countRow(y+1, mask|pe.Mask, require|pe.Require, counter) 302 | } 303 | } 304 | 305 | func (e *Enumerator) countCol(x int, mask, require uint64, counter *uint64) { 306 | if x >= e.width { 307 | if mask&require != require { 308 | return 309 | } 310 | *counter++ 311 | return 312 | } 313 | for _, pe := range e.colEntries[x] { 314 | if mask&pe.Mask != 0 { 315 | continue 316 | } 317 | e.countCol(x+1, mask|pe.Mask, require|pe.Require, counter) 318 | } 319 | } 320 | -------------------------------------------------------------------------------- /generator.go: -------------------------------------------------------------------------------- 1 | package rush 2 | 3 | import "fmt" 4 | 5 | type Generator struct { 6 | Width int 7 | Height int 8 | PrimarySize int 9 | PrimaryRow int 10 | // MinPieces int 11 | // MaxPieces int 12 | // MinSize int 13 | // MaxSize int 14 | } 15 | 16 | func NewDefaultGenerator() *Generator { 17 | return &Generator{6, 6, 2, 2} 18 | } 19 | 20 | func (g *Generator) Generate(iterations int) *Board { 21 | // create empty board 22 | board := NewEmptyBoard(g.Width, g.Height) 23 | 24 | // place the primary piece 25 | board.AddPiece(Piece{g.PrimaryRow * g.Width, g.PrimarySize, Horizontal}) 26 | 27 | // simulated annealing 28 | board = anneal(board, 20, 0.5, iterations) 29 | 30 | // unsolve step 31 | before := NewSolver(board).Solve().NumMoves 32 | board, _ = NewUnsolver(board).Unsolve() 33 | after := NewSolver(board).Solve().NumMoves 34 | fmt.Println(before, after) 35 | 36 | return board 37 | } 38 | -------------------------------------------------------------------------------- /graph.go: -------------------------------------------------------------------------------- 1 | package rush 2 | 3 | import ( 4 | "fmt" 5 | "sort" 6 | ) 7 | 8 | type Link struct { 9 | Src, Dst int 10 | } 11 | 12 | func MakeLink(id1, id2 int) Link { 13 | if id1 > id2 { 14 | id1, id2 = id2, id1 15 | } 16 | return Link{id1, id2} 17 | } 18 | 19 | func Graph(input *Board) { 20 | ids := make(map[MemoKey]int) 21 | numMovesToIDs := make(map[int][]int) 22 | idToNumMoves := make(map[int]int) 23 | var solutionIDs []int 24 | idCounter := 0 25 | linksToNonSolved := make(map[int]bool) 26 | 27 | var q []*Board 28 | q = append(q, input) 29 | 30 | fmt.Println("digraph g {") 31 | fmt.Println("pad=1;") 32 | var maxMoves int 33 | solver := NewSolverWithStaticAnalyzer(input, nil) 34 | for len(q) > 0 { 35 | board := q[len(q)-1] 36 | key := board.MemoKey() 37 | q = q[:len(q)-1] 38 | if _, ok := ids[*key]; ok { 39 | continue 40 | } 41 | id := idCounter 42 | idCounter++ 43 | ids[*key] = id 44 | solver.board = board 45 | numMoves := solver.UnsafeSolve().NumMoves 46 | if numMoves > maxMoves { 47 | maxMoves = numMoves 48 | } 49 | numMovesToIDs[numMoves] = append(numMovesToIDs[numMoves], id) 50 | idToNumMoves[id] = numMoves 51 | moves := board.Moves(nil) 52 | for _, move := range moves { 53 | board.DoMove(move) 54 | q = append(q, board.Copy()) 55 | board.UndoMove(move) 56 | } 57 | } 58 | 59 | board, solution := input.Unsolve() 60 | for _, move := range solution.Moves { 61 | solutionIDs = append(solutionIDs, ids[*board.MemoKey()]) 62 | board.DoMove(move) 63 | } 64 | solutionIDs = append(solutionIDs, ids[*board.MemoKey()]) 65 | 66 | links := make(map[Link]bool) 67 | solutionLinks := make(map[Link]bool) 68 | for j := 1; j < len(solutionIDs); j++ { 69 | i := j - 1 70 | link := MakeLink(solutionIDs[i], solutionIDs[j]) 71 | solutionLinks[link] = true 72 | } 73 | 74 | q = append(q, input) 75 | for len(q) > 0 { 76 | board := q[len(q)-1] 77 | key := board.MemoKey() 78 | q = q[:len(q)-1] 79 | id1 := ids[*key] 80 | moves := board.Moves(nil) 81 | for _, move := range moves { 82 | board.DoMove(move) 83 | id2 := ids[*board.MemoKey()] 84 | link := MakeLink(id1, id2) 85 | if _, ok := links[link]; !ok { 86 | links[link] = true 87 | numMoves1 := idToNumMoves[id1] 88 | numMoves2 := idToNumMoves[id2] 89 | if numMoves1 != 0 || numMoves2 != 0 { 90 | linksToNonSolved[id1] = true 91 | linksToNonSolved[id2] = true 92 | } 93 | q = append(q, board.Copy()) 94 | } 95 | board.UndoMove(move) 96 | } 97 | } 98 | 99 | var sortedLinks []Link 100 | for link := range links { 101 | sortedLinks = append(sortedLinks, link) 102 | } 103 | 104 | sort.Slice(sortedLinks, func(i, j int) bool { 105 | a, b := sortedLinks[i], sortedLinks[j] 106 | if a.Src == b.Src { 107 | return a.Dst < b.Dst 108 | } 109 | return a.Src < b.Src 110 | }) 111 | 112 | for id := 0; id < idCounter; id++ { 113 | if !linksToNonSolved[id] { 114 | continue 115 | } 116 | fmt.Printf("%d [label=\"\" shape=circle style=filled fillcolor=\"#EDD569\"];\n", id) 117 | } 118 | 119 | for _, link := range sortedLinks { 120 | a := link.Src 121 | b := link.Dst 122 | weight := 1 123 | if _, ok := solutionLinks[link]; ok { 124 | weight = 100 125 | } 126 | if !linksToNonSolved[a] || !linksToNonSolved[b] { 127 | continue 128 | } 129 | if idToNumMoves[a] > idToNumMoves[b] { 130 | fmt.Printf("%d -> %d [arrowsize=0.5, weight=%d];\n", a, b, weight) 131 | } else if idToNumMoves[a] < idToNumMoves[b] { 132 | fmt.Printf("%d -> %d [arrowsize=0.5, weight=%d];\n", b, a, weight) 133 | } else { 134 | fmt.Printf("%d -> %d [constraint=false, arrowhead=none, color=\"#00000020\"];\n", a, b) 135 | } 136 | } 137 | for _, ids := range numMovesToIDs { 138 | fmt.Printf("{ rank=same; ") 139 | first := true 140 | for _, id := range ids { 141 | if !linksToNonSolved[id] { 142 | continue 143 | } 144 | if !first { 145 | fmt.Printf(", ") 146 | } 147 | fmt.Printf("%d", id) 148 | first = false 149 | } 150 | fmt.Println(" }") 151 | } 152 | for _, id := range solutionIDs { 153 | fmt.Printf("%d [style=filled, fillcolor = \"#3F628F\"];\n", id) 154 | } 155 | for _, id := range numMovesToIDs[maxMoves] { 156 | fmt.Printf("%d [style=filled, fillcolor = \"#E94128\"];\n", id) 157 | } 158 | for _, id := range numMovesToIDs[0] { 159 | if !linksToNonSolved[id] { 160 | continue 161 | } 162 | fmt.Printf("%d [style=filled, fillcolor = \"#458955\"];\n", id) 163 | } 164 | fmt.Println("}") 165 | } 166 | -------------------------------------------------------------------------------- /memo.go: -------------------------------------------------------------------------------- 1 | package rush 2 | 3 | type MemoKey [MaxPieces]int 4 | 5 | func MakeMemoKey(pieces []Piece) MemoKey { 6 | var key MemoKey 7 | for i, piece := range pieces { 8 | key[i] = piece.Position 9 | } 10 | return key 11 | } 12 | 13 | func (a *MemoKey) Less(b *MemoKey, primary bool) bool { 14 | var i int 15 | if !primary { 16 | i++ 17 | } 18 | for ; i < MaxPieces; i++ { 19 | if a[i] != b[i] { 20 | return a[i] < b[i] 21 | } 22 | } 23 | return false 24 | } 25 | 26 | type Memo struct { 27 | data map[MemoKey]int 28 | hits uint64 29 | } 30 | 31 | func NewMemo() *Memo { 32 | data := make(map[MemoKey]int) 33 | return &Memo{data, 0} 34 | } 35 | 36 | func (memo *Memo) Size() int { 37 | return len(memo.data) 38 | } 39 | 40 | func (memo *Memo) Hits() uint64 { 41 | return memo.hits 42 | } 43 | 44 | func (memo *Memo) Add(key *MemoKey, depth int) bool { 45 | memo.hits++ 46 | if before, ok := memo.data[*key]; ok && before >= depth { 47 | return false 48 | } 49 | memo.data[*key] = depth 50 | return true 51 | } 52 | 53 | func (memo *Memo) Set(key *MemoKey, depth int) { 54 | memo.data[*key] = depth 55 | } 56 | -------------------------------------------------------------------------------- /model.go: -------------------------------------------------------------------------------- 1 | package rush 2 | 3 | import ( 4 | "fmt" 5 | "image" 6 | "math" 7 | "math/rand" 8 | "sort" 9 | "strings" 10 | ) 11 | 12 | // Orientation indicates which direction a Piece can move. Horizontal pieces 13 | // can move left and right. Vertical pieces can move up and down. 14 | type Orientation int 15 | 16 | const ( 17 | Horizontal Orientation = iota 18 | Vertical 19 | ) 20 | 21 | // Piece represents a piece (a car or a truck) on the grid. Its position is 22 | // a zero-indexed int, 0 <= Position < W*H. Its size specifies how many cells 23 | // it occupies. Its orientation specifies whether it is vertical or horizontal. 24 | type Piece struct { 25 | Position int 26 | Size int 27 | Orientation Orientation 28 | } 29 | 30 | func (piece *Piece) Stride(w int) int { 31 | if piece.Orientation == Horizontal { 32 | return 1 33 | } 34 | return w 35 | } 36 | 37 | func (piece *Piece) Row(w int) int { 38 | return piece.Position / w 39 | } 40 | 41 | func (piece *Piece) Col(w int) int { 42 | return piece.Position % w 43 | } 44 | 45 | // Move represents a move to make on the board. Piece indicates which piece 46 | // (by index) to move and Steps is a non-zero positive or negative int that 47 | // specifies how many cells to move the piece. 48 | type Move struct { 49 | Piece int 50 | Steps int 51 | } 52 | 53 | func (move Move) AbsSteps() int { 54 | if move.Steps < 0 { 55 | return -move.Steps 56 | } 57 | return move.Steps 58 | } 59 | 60 | func (move Move) Label() string { 61 | return string('A' + move.Piece) 62 | } 63 | 64 | func (move Move) String() string { 65 | return fmt.Sprintf("%s%+d", move.Label(), move.Steps) 66 | } 67 | 68 | // Board represents the complete puzzle state. The size of the grid, the 69 | // placement, size, orientation of the pieces. The placement of walls 70 | // (immovable obstacles). Which cells are occupied, either by a piece or a 71 | // wall. 72 | type Board struct { 73 | Width int 74 | Height int 75 | Pieces []Piece 76 | Walls []int 77 | occupied []bool 78 | memoKey MemoKey 79 | } 80 | 81 | func NewEmptyBoard(w, h int) *Board { 82 | occupied := make([]bool, w*h) 83 | memoKey := MakeMemoKey(nil) 84 | return &Board{w, h, nil, nil, occupied, memoKey} 85 | } 86 | 87 | func NewRandomBoard(w, h, primaryRow, primarySize, numPieces, numWalls int) *Board { 88 | board := NewEmptyBoard(w, h) 89 | board.AddPiece(Piece{primaryRow * w, primarySize, Horizontal}) 90 | for i := 1; i < numPieces; i++ { 91 | board.mutateAddPiece(100) 92 | } 93 | for i := 0; i < numWalls; i++ { 94 | board.mutateAddWall(100) 95 | } 96 | return board 97 | } 98 | 99 | func NewBoardFromString(desc string) (*Board, error) { 100 | s := int(math.Sqrt(float64(len(desc)))) 101 | if s*s != len(desc) { 102 | return nil, fmt.Errorf("NewBoardFromString only supports square boards") 103 | } 104 | rows := make([]string, s) 105 | for i := range rows { 106 | rows[i] = desc[i*s : i*s+s] 107 | } 108 | return NewBoard(rows) 109 | } 110 | 111 | func NewBoard(desc []string) (*Board, error) { 112 | // determine board size 113 | h := len(desc) 114 | if h < MinBoardSize { 115 | return nil, fmt.Errorf("board height must be >= %d", MinBoardSize) 116 | } 117 | w := len(desc[0]) 118 | if w < MinBoardSize { 119 | return nil, fmt.Errorf("board width must be >= %d", MinBoardSize) 120 | } 121 | 122 | // identify occupied cells and their labels 123 | occupied := make([]bool, w*h) 124 | positions := make(map[string][]int) 125 | var walls []int 126 | for y, row := range desc { 127 | for x, value := range row { 128 | label := string(value) 129 | if label == "." || label == "o" { 130 | continue 131 | } 132 | i := y*w + x 133 | occupied[i] = true 134 | if label == "x" { 135 | walls = append(walls, i) 136 | } else { 137 | positions[label] = append(positions[label], i) 138 | } 139 | 140 | } 141 | } 142 | 143 | // find and sort distinct piece labels 144 | labels := make([]string, 0, len(positions)) 145 | for label := range positions { 146 | labels = append(labels, label) 147 | } 148 | sort.Strings(labels) 149 | 150 | // validate and create pieces 151 | pieces := make([]Piece, 0, len(labels)) 152 | for _, label := range labels { 153 | ps := positions[label] 154 | if len(ps) < MinPieceSize { 155 | return nil, fmt.Errorf("piece %s length must be >= %d", label, MinPieceSize) 156 | } 157 | stride := ps[1] - ps[0] 158 | if stride != 1 && stride != w { 159 | return nil, fmt.Errorf("piece %s has invalid shape", label) 160 | } 161 | for i := 2; i < len(ps); i++ { 162 | if ps[i]-ps[i-1] != stride { 163 | return nil, fmt.Errorf("piece %s has invalid shape", label) 164 | } 165 | } 166 | dir := Horizontal 167 | if stride != 1 { 168 | dir = Vertical 169 | } 170 | pieces = append(pieces, Piece{ps[0], len(ps), dir}) 171 | } 172 | 173 | // create board 174 | board := &Board{w, h, pieces, walls, occupied, MakeMemoKey(pieces)} 175 | return board, board.Validate() 176 | } 177 | 178 | func (board *Board) String() string { 179 | w := board.Width 180 | h := board.Height 181 | grid := make([]string, w*h) 182 | for i := range grid { 183 | grid[i] = "." 184 | } 185 | for _, i := range board.Walls { 186 | grid[i] = "x" 187 | } 188 | for i, piece := range board.Pieces { 189 | label := string('A' + i) 190 | idx := piece.Position 191 | stride := piece.Stride(w) 192 | for j := 0; j < piece.Size; j++ { 193 | grid[idx] = label 194 | idx += stride 195 | } 196 | } 197 | rows := make([]string, h) 198 | for y := 0; y < h; y++ { 199 | i := y * w 200 | rows[y] = strings.Join(grid[i:i+w], "") 201 | } 202 | return strings.Join(rows, "\n") 203 | } 204 | 205 | func (board *Board) Hash() string { 206 | w := board.Width 207 | h := board.Height 208 | grid := make([]rune, w*h) 209 | for i := range grid { 210 | grid[i] = '.' 211 | } 212 | for _, i := range board.Walls { 213 | grid[i] = 'x' 214 | } 215 | for i, piece := range board.Pieces { 216 | label := rune('A' + i) 217 | idx := piece.Position 218 | stride := 1 219 | if piece.Orientation == Vertical { 220 | stride = w 221 | } 222 | for j := 0; j < piece.Size; j++ { 223 | grid[idx] = label 224 | idx += stride 225 | } 226 | } 227 | return string(grid) 228 | } 229 | 230 | func (board *Board) Copy() *Board { 231 | w := board.Width 232 | h := board.Height 233 | pieces := make([]Piece, len(board.Pieces)) 234 | walls := make([]int, len(board.Walls)) 235 | occupied := make([]bool, len(board.occupied)) 236 | memoKey := board.memoKey 237 | copy(pieces, board.Pieces) 238 | copy(walls, board.Walls) 239 | copy(occupied, board.occupied) 240 | return &Board{w, h, pieces, walls, occupied, memoKey} 241 | } 242 | 243 | func (board *Board) SortPieces() { 244 | a := board.Pieces[1:] 245 | sort.Slice(a, func(i, j int) bool { 246 | return a[i].Position < a[j].Position 247 | }) 248 | board.memoKey = MakeMemoKey(board.Pieces) 249 | } 250 | 251 | func (board *Board) HasFullRowOrCol() bool { 252 | w := board.Width 253 | h := board.Height 254 | for y := 0; y < h; y++ { 255 | var size int 256 | for _, piece := range board.Pieces { 257 | if piece.Orientation == Horizontal && piece.Row(w) == y { 258 | size += piece.Size 259 | } 260 | 261 | } 262 | if size == w { 263 | return true 264 | } 265 | } 266 | for x := 0; x < w; x++ { 267 | var size int 268 | for _, piece := range board.Pieces { 269 | if piece.Orientation == Vertical && piece.Col(w) == x { 270 | size += piece.Size 271 | } 272 | } 273 | if size == h { 274 | return true 275 | } 276 | } 277 | return false 278 | } 279 | 280 | func (board *Board) Validate() error { 281 | w := board.Width 282 | h := board.Height 283 | pieces := board.Pieces 284 | 285 | // board size must be >= MinBoardSize 286 | if w < MinBoardSize { 287 | return fmt.Errorf("board width must be >= %d", MinBoardSize) 288 | } 289 | if h < MinBoardSize { 290 | return fmt.Errorf("board height must be >= %d", MinBoardSize) 291 | } 292 | 293 | // board must have at least one piece 294 | if len(pieces) < 1 { 295 | return fmt.Errorf("board must have at least one piece") 296 | } 297 | 298 | // board must have <= MaxPieces 299 | if len(pieces) > MaxPieces { 300 | return fmt.Errorf("board must have <= %d pieces", MaxPieces) 301 | } 302 | 303 | // primary piece must be horizontal 304 | if pieces[0].Orientation != Horizontal { 305 | return fmt.Errorf("primary piece must be horizontal") 306 | } 307 | 308 | // validate walls 309 | occupied := make([]bool, w*h) 310 | for _, i := range board.Walls { 311 | // wall must be inside the grid 312 | if i < 0 || i >= w*h { 313 | return fmt.Errorf("a wall is outside of the grid") 314 | } 315 | 316 | // walls must not intersect 317 | if occupied[i] { 318 | return fmt.Errorf("a wall intersects another wall") 319 | } 320 | occupied[i] = true 321 | } 322 | 323 | // validate pieces 324 | primaryRow := pieces[0].Row(w) 325 | for i, piece := range pieces { 326 | label := string('A' + i) 327 | row := piece.Row(w) 328 | col := piece.Col(w) 329 | 330 | // piece size must be >= MinPieceSize 331 | if piece.Size < MinPieceSize { 332 | return fmt.Errorf("piece %s must have size >= %d", label, MinPieceSize) 333 | } 334 | 335 | // no horizontal pieces can be on the same row as the primary piece 336 | if i > 0 && piece.Orientation == Horizontal && row == primaryRow { 337 | return fmt.Errorf("no horizontal pieces can be on the primary row") 338 | } 339 | 340 | // pieces must be contained within the grid 341 | if piece.Orientation == Horizontal { 342 | if row < 0 || row >= h || col < 0 || col+piece.Size > w { 343 | return fmt.Errorf("piece %s is outside of the grid", label) 344 | } 345 | } else { 346 | if col < 0 || col >= w || row < 0 || row+piece.Size > h { 347 | return fmt.Errorf("piece %s is outside of the grid", label) 348 | } 349 | } 350 | 351 | // pieces must not intersect 352 | idx := piece.Position 353 | stride := piece.Stride(w) 354 | for j := 0; j < piece.Size; j++ { 355 | if occupied[idx] { 356 | return fmt.Errorf("piece %s intersects with another piece", label) 357 | } 358 | occupied[idx] = true 359 | idx += stride 360 | } 361 | } 362 | 363 | return nil 364 | } 365 | 366 | func (board *Board) isOccupied(piece Piece) bool { 367 | idx := piece.Position 368 | stride := piece.Stride(board.Width) 369 | for i := 0; i < piece.Size; i++ { 370 | if board.occupied[idx] { 371 | return true 372 | } 373 | idx += stride 374 | } 375 | return false 376 | } 377 | 378 | func (board *Board) setOccupied(piece Piece, value bool) { 379 | idx := piece.Position 380 | stride := piece.Stride(board.Width) 381 | for i := 0; i < piece.Size; i++ { 382 | board.occupied[idx] = value 383 | idx += stride 384 | } 385 | } 386 | 387 | func (board *Board) addPiece(piece Piece) { 388 | i := len(board.Pieces) 389 | board.Pieces = append(board.Pieces, piece) 390 | board.setOccupied(piece, true) 391 | board.memoKey[i] = piece.Position 392 | } 393 | 394 | func (board *Board) AddPiece(piece Piece) bool { 395 | if board.isOccupied(piece) { 396 | return false 397 | } 398 | board.addPiece(piece) 399 | return true 400 | } 401 | 402 | func (board *Board) AddWall(i int) bool { 403 | if board.occupied[i] { 404 | return false 405 | } 406 | board.Walls = append(board.Walls, i) 407 | board.occupied[i] = true 408 | return true 409 | } 410 | 411 | func (board *Board) RemovePiece(i int) { 412 | board.setOccupied(board.Pieces[i], false) 413 | j := len(board.Pieces) - 1 414 | board.Pieces[i] = board.Pieces[j] 415 | board.memoKey[i] = board.Pieces[i].Position 416 | board.Pieces = board.Pieces[:j] 417 | board.memoKey[j] = 0 418 | } 419 | 420 | func (board *Board) RemoveLastPiece() { 421 | board.RemovePiece(len(board.Pieces) - 1) 422 | } 423 | 424 | func (board *Board) RemoveWall(i int) { 425 | board.occupied[board.Walls[i]] = false 426 | a := board.Walls 427 | a[i] = a[len(a)-1] 428 | a = a[:len(a)-1] 429 | board.Walls = a 430 | } 431 | 432 | func (board *Board) Target() int { 433 | w := board.Width 434 | piece := board.Pieces[0] 435 | row := piece.Row(w) 436 | return (row+1)*w - piece.Size 437 | } 438 | 439 | func (board *Board) Moves(buf []Move) []Move { 440 | moves := buf[:0] 441 | w := board.Width 442 | h := board.Height 443 | for i, piece := range board.Pieces { 444 | var stride, reverseSteps, forwardSteps int 445 | if piece.Orientation == Vertical { 446 | y := piece.Position / w 447 | reverseSteps = -y 448 | forwardSteps = h - piece.Size - y 449 | stride = w 450 | } else { 451 | x := piece.Position % w 452 | reverseSteps = -x 453 | forwardSteps = w - piece.Size - x 454 | stride = 1 455 | } 456 | // reverse (negative steps) 457 | idx := piece.Position - stride 458 | for steps := -1; steps >= reverseSteps; steps-- { 459 | if board.occupied[idx] { 460 | break 461 | } 462 | moves = append(moves, Move{i, steps}) 463 | idx -= stride 464 | } 465 | // forward (positive steps) 466 | idx = piece.Position + piece.Size*stride 467 | for steps := 1; steps <= forwardSteps; steps++ { 468 | if board.occupied[idx] { 469 | break 470 | } 471 | moves = append(moves, Move{i, steps}) 472 | idx += stride 473 | } 474 | } 475 | return moves 476 | } 477 | 478 | func (board *Board) DoMove(move Move) { 479 | piece := &board.Pieces[move.Piece] 480 | stride := piece.Stride(board.Width) 481 | 482 | idx := piece.Position 483 | for i := 0; i < piece.Size; i++ { 484 | board.occupied[idx] = false 485 | idx += stride 486 | } 487 | 488 | piece.Position += stride * move.Steps 489 | board.memoKey[move.Piece] = piece.Position 490 | 491 | idx = piece.Position 492 | for i := 0; i < piece.Size; i++ { 493 | board.occupied[idx] = true 494 | idx += stride 495 | } 496 | } 497 | 498 | func (board *Board) UndoMove(move Move) { 499 | board.DoMove(Move{move.Piece, -move.Steps}) 500 | } 501 | 502 | func (board *Board) StateIterator() <-chan *Board { 503 | ch := make(chan *Board, 16) 504 | board = board.Copy() 505 | memo := NewMemo() 506 | var f func(int, int) 507 | f = func(depth, previousPiece int) { 508 | if !memo.Add(board.MemoKey(), 0) { 509 | return 510 | } 511 | ch <- board.Copy() 512 | for _, move := range board.Moves(nil) { 513 | if move.Piece == previousPiece { 514 | continue 515 | } 516 | board.DoMove(move) 517 | f(depth+1, move.Piece) 518 | board.UndoMove(move) 519 | } 520 | if depth == 0 { 521 | close(ch) 522 | } 523 | } 524 | go f(0, -1) 525 | return ch 526 | } 527 | 528 | func (board *Board) ReachableStates() int { 529 | var count int 530 | memo := NewMemo() 531 | var f func(int) 532 | f = func(previousPiece int) { 533 | if !memo.Add(board.MemoKey(), 0) { 534 | return 535 | } 536 | count++ 537 | for _, move := range board.Moves(nil) { 538 | if move.Piece == previousPiece { 539 | continue 540 | } 541 | board.DoMove(move) 542 | f(move.Piece) 543 | board.UndoMove(move) 544 | } 545 | } 546 | f(-1) 547 | return count 548 | } 549 | 550 | func (board *Board) MemoKey() *MemoKey { 551 | return &board.memoKey 552 | } 553 | 554 | func (board *Board) Solve() Solution { 555 | return NewSolver(board).Solve() 556 | } 557 | 558 | func (board *Board) Unsolve() (*Board, Solution) { 559 | return NewUnsolver(board).Unsolve() 560 | } 561 | 562 | func (board *Board) UnsafeSolve() Solution { 563 | return NewSolver(board).UnsafeSolve() 564 | } 565 | 566 | func (board *Board) UnsafeUnsolve() (*Board, Solution) { 567 | return NewUnsolver(board).UnsafeUnsolve() 568 | } 569 | 570 | func (board *Board) Render() image.Image { 571 | return renderBoard(board) 572 | } 573 | 574 | func (board *Board) Impossible() bool { 575 | return theStaticAnalyzer.Impossible(board) 576 | } 577 | 578 | func (board *Board) BlockedSquares() []int { 579 | return theStaticAnalyzer.BlockedSquares(board) 580 | } 581 | 582 | func (board *Board) Canonicalize() *Board { 583 | bestKey := board.memoKey 584 | bestBoard := board.Copy() 585 | for b := range board.StateIterator() { 586 | if b.memoKey.Less(&bestKey, true) { 587 | bestKey = b.memoKey 588 | bestBoard = b.Copy() 589 | } 590 | } 591 | bestBoard.SortPieces() 592 | return bestBoard 593 | } 594 | 595 | // random board mutation below 596 | 597 | func (board *Board) Energy() float64 { 598 | solution := board.Solve() 599 | if !solution.Solvable { 600 | return 1 601 | } 602 | e := float64(solution.NumMoves) 603 | e += float64(solution.NumSteps) / 100 604 | return -e 605 | } 606 | 607 | type UndoFunc func() 608 | 609 | func (board *Board) Mutate() UndoFunc { 610 | const maxAttempts = 100 611 | for { 612 | var undo UndoFunc 613 | switch rand.Intn(7 + 3) { 614 | case 0: 615 | undo = board.mutateAddPiece(maxAttempts) 616 | case 1: 617 | undo = board.mutateAddWall(maxAttempts) 618 | case 2: 619 | undo = board.mutateRemovePiece() 620 | case 3: 621 | undo = board.mutateRemoveWall() 622 | case 4: 623 | undo = board.mutateRemoveAndAddPiece(maxAttempts) 624 | case 5: 625 | undo = board.mutateRemoveAndAddWall(maxAttempts) 626 | default: 627 | undo = board.mutateMakeMove() 628 | } 629 | if undo != nil { 630 | return undo 631 | } 632 | } 633 | } 634 | 635 | func (board *Board) mutateMakeMove() UndoFunc { 636 | moves := board.Moves(nil) 637 | if len(moves) == 0 { 638 | return nil 639 | } 640 | move := moves[rand.Intn(len(moves))] 641 | board.DoMove(move) 642 | return func() { 643 | board.UndoMove(move) 644 | } 645 | } 646 | 647 | func (board *Board) mutateAddPiece(maxAttempts int) UndoFunc { 648 | if len(board.Pieces) >= 8 { 649 | return nil 650 | } 651 | piece, ok := board.randomPiece(maxAttempts) 652 | if !ok { 653 | return nil 654 | } 655 | i := len(board.Pieces) 656 | board.AddPiece(piece) 657 | return func() { 658 | board.RemovePiece(i) 659 | } 660 | } 661 | 662 | func (board *Board) mutateAddWall(maxAttempts int) UndoFunc { 663 | if len(board.Walls) >= 0 { 664 | return nil 665 | } 666 | wall, ok := board.randomWall(maxAttempts) 667 | if !ok { 668 | return nil 669 | } 670 | i := len(board.Walls) 671 | board.AddWall(wall) 672 | return func() { 673 | board.RemoveWall(i) 674 | } 675 | } 676 | 677 | func (board *Board) mutateRemovePiece() UndoFunc { 678 | // never remove the primary piece 679 | if len(board.Pieces) < 2 { 680 | return nil 681 | } 682 | i := rand.Intn(len(board.Pieces)-1) + 1 683 | piece := board.Pieces[i] 684 | board.RemovePiece(i) 685 | return func() { 686 | board.AddPiece(piece) 687 | } 688 | } 689 | 690 | func (board *Board) mutateRemoveWall() UndoFunc { 691 | if len(board.Walls) == 0 { 692 | return nil 693 | } 694 | i := rand.Intn(len(board.Walls)) 695 | wall := board.Walls[i] 696 | board.RemoveWall(i) 697 | return func() { 698 | board.AddWall(wall) 699 | } 700 | } 701 | 702 | func (board *Board) mutateRemoveAndAddPiece(maxAttempts int) UndoFunc { 703 | undoRemove := board.mutateRemovePiece() 704 | if undoRemove == nil { 705 | return nil 706 | } 707 | undoAdd := board.mutateAddPiece(maxAttempts) 708 | if undoAdd == nil { 709 | return undoRemove 710 | } 711 | return func() { 712 | undoAdd() 713 | undoRemove() 714 | } 715 | } 716 | 717 | func (board *Board) mutateRemoveAndAddWall(maxAttempts int) UndoFunc { 718 | undoRemove := board.mutateRemoveWall() 719 | if undoRemove == nil { 720 | return nil 721 | } 722 | undoAdd := board.mutateAddWall(maxAttempts) 723 | if undoAdd == nil { 724 | return undoRemove 725 | } 726 | return func() { 727 | undoAdd() 728 | undoRemove() 729 | } 730 | } 731 | 732 | func (board *Board) randomPiece(maxAttempts int) (Piece, bool) { 733 | w := board.Width 734 | h := board.Height 735 | for i := 0; i < maxAttempts; i++ { 736 | size := 2 + rand.Intn(2) // TODO: weighted? 737 | orientation := Orientation(rand.Intn(2)) 738 | var x, y int 739 | if orientation == Vertical { 740 | x = rand.Intn(w) 741 | y = rand.Intn(h - size + 1) 742 | } else { 743 | x = rand.Intn(w - size + 1) 744 | y = rand.Intn(h) 745 | } 746 | position := y*w + x 747 | piece := Piece{position, size, orientation} 748 | if !board.isOccupied(piece) { 749 | return piece, true 750 | } 751 | } 752 | return Piece{}, false 753 | } 754 | 755 | func (board *Board) randomWall(maxAttempts int) (int, bool) { 756 | n := board.Width * board.Height 757 | for i := 0; i < maxAttempts; i++ { 758 | p := rand.Intn(n) 759 | if !board.occupied[p] { 760 | return p, true 761 | } 762 | } 763 | return 0, false 764 | } 765 | -------------------------------------------------------------------------------- /render.go: -------------------------------------------------------------------------------- 1 | package rush 2 | 3 | import ( 4 | "fmt" 5 | "image" 6 | "math" 7 | "strings" 8 | 9 | "github.com/fogleman/gg" 10 | ) 11 | 12 | const ( 13 | showBlockedSquares = false 14 | showPieceLabels = false 15 | showSolution = false 16 | ) 17 | 18 | const ( 19 | cellSize = 160 20 | padding = 32 21 | labelFontSize = 36 22 | footerFontSize = 24 23 | ) 24 | 25 | const ( 26 | backgroundColor = "FFFFFF" 27 | boardColor = "F2EACD" 28 | blockedColor = "D96D60" 29 | gridLineColor = "222222" 30 | primaryPieceColor = "CC3333" 31 | pieceColor = "338899" 32 | pieceOutlineColor = "222222" 33 | labelColor = "222222" 34 | wallColor = "222222" 35 | ) 36 | 37 | const labelFont = "/Library/Fonts/Arial.ttf" 38 | const footerFont = "/System/Library/Fonts/Menlo.ttc" 39 | 40 | func renderBoard(board *Board) image.Image { 41 | const S = cellSize 42 | bw := board.Width 43 | bh := board.Height 44 | w := bw * S 45 | h := bh * S 46 | iw := w + padding*2 47 | ih := h + padding*2 48 | if showSolution { 49 | ih += 120 50 | } 51 | dc := gg.NewContext(iw, ih) 52 | dc.LoadFontFace(labelFont, labelFontSize) 53 | dc.Translate(padding, padding) 54 | dc.SetHexColor(backgroundColor) 55 | dc.Clear() 56 | dc.SetHexColor(boardColor) 57 | dc.DrawRectangle(0, 0, float64(w+1), float64(h+1)) 58 | dc.Fill() 59 | if showBlockedSquares { 60 | for _, i := range board.BlockedSquares() { 61 | x := float64(i % bw) 62 | y := float64(i / bw) 63 | dc.DrawRectangle(x*S, y*S, S, S) 64 | } 65 | dc.SetHexColor(blockedColor) 66 | dc.Fill() 67 | } 68 | ex := float64(bw) * S 69 | ey := float64(board.Pieces[0].Row(bw))*S + S/2 70 | es := float64(S) / 10 71 | dc.LineTo(ex, ey+es) 72 | dc.LineTo(ex, ey-es) 73 | dc.LineTo(ex+es, ey) 74 | dc.SetHexColor(gridLineColor) 75 | dc.Fill() 76 | p := S / 8.0 77 | r := S / 32.0 78 | for _, i := range board.Walls { 79 | x := float64(i % bw) 80 | y := float64(i / bw) 81 | dc.DrawRectangle(x*S, y*S, S, S) 82 | dc.SetHexColor(wallColor) 83 | dc.Fill() 84 | dc.DrawCircle(x*S+p, y*S+p, r) 85 | dc.DrawCircle(x*S+S-p, y*S+p, r) 86 | dc.DrawCircle(x*S+p, y*S+S-p, r) 87 | dc.DrawCircle(x*S+S-p, y*S+S-p, r) 88 | dc.SetHexColor(boardColor) 89 | dc.Fill() 90 | } 91 | for x := S; x < w; x += S { 92 | fx := float64(x) 93 | dc.DrawLine(fx, 0, fx, float64(h)) 94 | } 95 | for y := S; y < h; y += S { 96 | fy := float64(y) 97 | dc.DrawLine(0, fy, float64(w), fy) 98 | } 99 | dc.SetHexColor(gridLineColor) 100 | dc.SetLineWidth(2) 101 | dc.Stroke() 102 | dc.DrawRectangle(0, 0, float64(w+1), float64(h+1)) 103 | dc.SetLineWidth(6) 104 | dc.Stroke() 105 | for i, piece := range board.Pieces { 106 | stride := 1 107 | if piece.Orientation == Vertical { 108 | stride = bw 109 | } 110 | i0 := piece.Position 111 | i1 := i0 + stride*(piece.Size-1) 112 | x0 := float64(i0 % bw) 113 | y0 := float64(i0 / bw) 114 | x1 := float64(i1 % bw) 115 | y1 := float64(i1 / bw) 116 | dx := x1 - x0 117 | dy := y1 - y0 118 | m := S / 8.0 119 | px := x0*S + m 120 | py := y0*S + m 121 | pw := dx*S + S - m*2 122 | ph := dy*S + S - m*2 123 | dc.DrawRoundedRectangle(px+0.5, py+0.5, pw, ph, S/8.0) 124 | if i == 0 { 125 | dc.SetHexColor(primaryPieceColor) 126 | } else { 127 | dc.SetHexColor(pieceColor) 128 | } 129 | dc.FillPreserve() 130 | dc.SetLineWidth(S / 32.0) 131 | dc.SetHexColor(pieceOutlineColor) 132 | dc.Stroke() 133 | if showPieceLabels { 134 | tx := px + pw/2 135 | ty := py + ph/2 136 | dc.SetHexColor(labelColor) 137 | dc.DrawStringAnchored(string('A'+i), tx, ty, 0.5, 0.5) 138 | } 139 | } 140 | 141 | if showSolution { 142 | x := float64(w) / 2 143 | y := float64(h) + padding*0.75 144 | footer := "" 145 | solution := board.Solve() 146 | if solution.Solvable { 147 | moveStrings := make([]string, len(solution.Moves)) 148 | for i, move := range solution.Moves { 149 | moveStrings[i] = move.String() 150 | } 151 | footer = fmt.Sprintf("%s (%d moves)", 152 | strings.Join(moveStrings, " "), solution.NumMoves) 153 | } 154 | dc.LoadFontFace(footerFont, footerFontSize) 155 | var tw float64 156 | for _, line := range dc.WordWrap(footer, float64(w)) { 157 | w, _ := dc.MeasureString(line) 158 | tw = math.Max(tw, w) 159 | } 160 | dc.DrawStringWrapped(footer, x, y, 0.5, 0, tw, 1.5, gg.AlignLeft) 161 | } 162 | 163 | return dc.Image() 164 | } 165 | -------------------------------------------------------------------------------- /server/.gitignore: -------------------------------------------------------------------------------- 1 | env 2 | *.db 3 | 4 | -------------------------------------------------------------------------------- /server/main.py: -------------------------------------------------------------------------------- 1 | from flask import Flask, jsonify, g 2 | 3 | import random 4 | import sqlite3 5 | 6 | # config 7 | 8 | DB_PATH = 'rush.db' 9 | DB_ATTR = '_db' 10 | 11 | # app 12 | 13 | app = Flask(__name__) 14 | 15 | # hooks 16 | 17 | def get_db(): 18 | db = getattr(g, DB_ATTR, None) 19 | if db is None: 20 | db = sqlite3.connect(DB_PATH) 21 | db.row_factory = sqlite3.Row 22 | setattr(g, DB_ATTR, db) 23 | return db 24 | 25 | def query_db(query, args=(), one=False): 26 | cursor = get_db().execute(query, args) 27 | result = cursor.fetchall() 28 | cursor.close() 29 | return (result[0] if result else None) if one else result 30 | 31 | def row_dict(row): 32 | return dict((k, row[k]) for k in row.keys()) 33 | 34 | @app.teardown_appcontext 35 | def close_connection(exception): 36 | db = getattr(g, DB_ATTR, None) 37 | if db is not None: 38 | db.close() 39 | 40 | # views 41 | 42 | @app.route('/random.json') 43 | def random_json(): 44 | COUNTS = [2577412,2577411,2577403,2577227,2575473,2563823,2518414,2412682,2236460,1990153,1696046,1396300,1128432,907256,727035,577576,452694,349953,267487,202610,152245,113712,84358,62386,46004,33870,25117,18538,13786,10224,7757,5919,4458,3398,2537,1883,1395,1022,776,567,425,326,234,171,113,85,63,47,33,23,15,13,11,8,4,2,2,2,1,1] 45 | i = random.randint(15, 40) - 1 46 | rowid = random.randint(1, COUNTS[i]) 47 | 48 | db = get_db() 49 | q = 'select * from rush where rowid = ?;' 50 | row = query_db(q, (rowid,), one=True) 51 | resp = jsonify(row_dict(row)) 52 | resp.headers['Access-Control-Allow-Origin'] = '*' 53 | return resp 54 | 55 | # main 56 | 57 | if __name__ == '__main__': 58 | app.run(debug=True) 59 | -------------------------------------------------------------------------------- /solver.go: -------------------------------------------------------------------------------- 1 | package rush 2 | 3 | type Solution struct { 4 | Solvable bool 5 | Moves []Move 6 | NumMoves int 7 | NumSteps int 8 | Depth int 9 | MemoSize int 10 | MemoHits uint64 11 | } 12 | 13 | type Solver struct { 14 | board *Board 15 | target int 16 | memo *Memo 17 | sa *StaticAnalyzer 18 | path []Move 19 | moves [][]Move 20 | } 21 | 22 | func NewSolverWithStaticAnalyzer(board *Board, sa *StaticAnalyzer) *Solver { 23 | solver := Solver{} 24 | solver.board = board 25 | solver.target = board.Target() 26 | solver.memo = NewMemo() 27 | solver.sa = sa 28 | return &solver 29 | } 30 | 31 | func NewSolver(board *Board) *Solver { 32 | return NewSolverWithStaticAnalyzer(board, theStaticAnalyzer) 33 | } 34 | 35 | func (solver *Solver) isSolved() bool { 36 | return solver.board.Pieces[0].Position == solver.target 37 | } 38 | 39 | func (solver *Solver) search(depth, maxDepth, previousPiece int) bool { 40 | height := maxDepth - depth 41 | if height == 0 { 42 | return solver.isSolved() 43 | } 44 | 45 | board := solver.board 46 | if !solver.memo.Add(board.MemoKey(), height) { 47 | return false 48 | } 49 | 50 | // count occupied squares between primary piece and target 51 | primary := board.Pieces[0] 52 | i0 := primary.Position + primary.Size 53 | i1 := solver.target + primary.Size - 1 54 | minMoves := 0 55 | for i := i0; i <= i1; i++ { 56 | if board.occupied[i] { 57 | minMoves++ 58 | } 59 | } 60 | if minMoves >= height { 61 | return false 62 | } 63 | 64 | buf := &solver.moves[depth] 65 | *buf = board.Moves(*buf) 66 | for _, move := range *buf { 67 | if move.Piece == previousPiece { 68 | continue 69 | } 70 | board.DoMove(move) 71 | solved := solver.search(depth+1, maxDepth, move.Piece) 72 | board.UndoMove(move) 73 | if solved { 74 | solver.memo.Set(board.MemoKey(), height-1) 75 | solver.path[depth] = move 76 | return true 77 | } 78 | } 79 | return false 80 | } 81 | 82 | func (solver *Solver) solve(skipChecks bool) Solution { 83 | board := solver.board 84 | memo := solver.memo 85 | 86 | if !skipChecks { 87 | if err := board.Validate(); err != nil { 88 | return Solution{} 89 | } 90 | if solver.sa.Impossible(board) { 91 | return Solution{} 92 | } 93 | } 94 | 95 | if solver.isSolved() { 96 | return Solution{Solvable: true} 97 | } 98 | 99 | previousMemoSize := 0 100 | noChange := 0 101 | cutoff := board.Width - board.Pieces[0].Size 102 | for i := 1; ; i++ { 103 | solver.path = make([]Move, i) 104 | solver.moves = make([][]Move, i) 105 | if solver.search(0, i, -1) { 106 | moves := solver.path 107 | steps := 0 108 | for _, move := range moves { 109 | steps += move.AbsSteps() 110 | } 111 | result := Solution{ 112 | Solvable: true, 113 | Moves: moves, 114 | NumMoves: len(moves), 115 | NumSteps: steps, 116 | Depth: i, 117 | MemoSize: memo.Size(), 118 | MemoHits: memo.Hits(), 119 | } 120 | return result 121 | } 122 | memoSize := memo.Size() 123 | if memoSize == previousMemoSize { 124 | noChange++ 125 | } else { 126 | noChange = 0 127 | } 128 | if !skipChecks && noChange > cutoff { 129 | return Solution{ 130 | Depth: i, 131 | MemoSize: memo.Size(), 132 | MemoHits: memo.Hits(), 133 | } 134 | } 135 | previousMemoSize = memoSize 136 | } 137 | } 138 | 139 | func (solver *Solver) Solve() Solution { 140 | return solver.solve(false) 141 | } 142 | 143 | func (solver *Solver) UnsafeSolve() Solution { 144 | return solver.solve(true) 145 | } 146 | -------------------------------------------------------------------------------- /static.go: -------------------------------------------------------------------------------- 1 | package rush 2 | 3 | /* 4 | 5 | Static analysis code is below. Its purpose is to detect if a Board will be 6 | impossible to solve without actually doing an expensive recursive search. 7 | Certain patterns, frequent among randomly generated boards, can be relatively 8 | easily detected and weeded out as impossible to solve. 9 | 10 | Consider the following row. We will analyze it in isolation. 11 | 12 | AAA.BB 13 | 14 | There are only three possible layouts for these two pieces: 15 | 16 | AAABB. 17 | AAA.BB 18 | .AAABB 19 | 20 | Of the six squares on this row, three of them are always occupied no matter 21 | the configuration of the pieces: 22 | 23 | .xx.x. 24 | 25 | We will call these squares "blocked." 26 | 27 | We can examine all rows and columns on the board for such "blocked" squares. 28 | 29 | If any of the squares between the primary piece (the "red car") and its exit 30 | are blocked, then we know that the puzzle cannot be solved. 31 | 32 | But that's not all! Blocked squares on a row affect the possibilities on the 33 | intersecting columns. Let's take the blocked squares from above and consider 34 | an example column: 35 | 36 | . 37 | . 38 | .xx.x. 39 | C 40 | C 41 | . 42 | 43 | Without considering blocked squares, it seems that the C piece could 44 | potentially traverse the entire column. Actually, the C piece will be 45 | constrained to the bottom two squares in the column, making the second from 46 | the bottom square also blocked: 47 | 48 | . 49 | . 50 | .xx.x. 51 | . 52 | x 53 | . 54 | 55 | We can repeat this process of identifying blocked squares based on each row 56 | and column's configuration and existing blocked squares until no new squares 57 | are identified. 58 | 59 | So we need an algorithm that can take the pieces present on a row or column, 60 | along with already-identified blocked squares from the perpendicular direction, 61 | and return a set of blocked squares. Returning to the column above: 62 | 63 | ..xCC. => ....x. 64 | 65 | Note that we need to distinguish between horizontally blocked squares and 66 | vertically blocked squares. So the example above does not retain blocked 67 | squares from its input. Here are some example inputs and outputs using our 68 | ASCII based representation: 69 | 70 | ..xAA. => ....x. 71 | AAA.BB => .xx.x. 72 | AA..BB => ...... 73 | .x.AA. => ...... 74 | 75 | Let's figure out the appropriate data structures. We'll use this example: 76 | 77 | 012345 012345 78 | ..xAA. => ....x. 79 | 80 | n = 6 # number of squares on the row (or column) 81 | blocked = [2] # blocked squares from the perpendicular orientation 82 | positions = [3] # positions of pieces 83 | sizes = [2] # sizes of pieces 84 | result = [4] # blocked squares found by the algorithm 85 | 86 | */ 87 | 88 | var theStaticAnalyzer = NewStaticAnalyzer() 89 | 90 | type StaticAnalyzer struct { 91 | // these buffers are allocated once so multiple static analyses can be 92 | // performed faster (less GC) 93 | horz []bool 94 | vert []bool 95 | positions []int 96 | sizes []int 97 | blocked []int 98 | lens []int 99 | idx []int 100 | counts []int 101 | result []int 102 | placements [][]int 103 | } 104 | 105 | func NewStaticAnalyzer() *StaticAnalyzer { 106 | maxPiecesPerRow := MaxBoardSize / MinPieceSize 107 | maxPlacementsPerRow := MaxBoardSize - MinPieceSize + 1 108 | sa := &StaticAnalyzer{} 109 | sa.horz = make([]bool, MaxBoardSize*MaxBoardSize) 110 | sa.vert = make([]bool, MaxBoardSize*MaxBoardSize) 111 | sa.positions = make([]int, maxPiecesPerRow) 112 | sa.sizes = make([]int, maxPiecesPerRow) 113 | sa.blocked = make([]int, MaxBoardSize) 114 | sa.lens = make([]int, maxPiecesPerRow) 115 | sa.idx = make([]int, maxPiecesPerRow) 116 | sa.counts = make([]int, MaxBoardSize) 117 | sa.result = make([]int, MaxBoardSize) 118 | sa.placements = make([][]int, maxPiecesPerRow) 119 | for i := range sa.placements { 120 | sa.placements[i] = make([]int, maxPlacementsPerRow) 121 | } 122 | return sa 123 | } 124 | 125 | func (sa *StaticAnalyzer) Impossible(board *Board) bool { 126 | // run analysis 127 | sa.analyze(board) 128 | // see if any squares between the primary piece and its exit are blocked 129 | w := board.Width 130 | piece := board.Pieces[0] 131 | i0 := piece.Position + piece.Size 132 | i1 := (piece.Row(w) + 1) * w 133 | for i := i0; i < i1; i++ { 134 | if sa.horz[i] || sa.vert[i] { 135 | return true 136 | } 137 | } 138 | return false 139 | } 140 | 141 | func (sa *StaticAnalyzer) BlockedSquares(board *Board) []int { 142 | // run analysis 143 | sa.analyze(board) 144 | // compile a list of all blocked squares 145 | n := board.Width * board.Height 146 | var result []int 147 | for i := 0; i < n; i++ { 148 | if sa.horz[i] || sa.vert[i] { 149 | result = append(result, i) 150 | } 151 | } 152 | return result 153 | } 154 | 155 | func (sa *StaticAnalyzer) analyze(board *Board) { 156 | // zero out buffers 157 | for i := range sa.horz { 158 | sa.horz[i] = false 159 | sa.vert[i] = false 160 | } 161 | // walls are always blocked for both directions 162 | for _, i := range board.Walls { 163 | sa.horz[i] = true 164 | sa.vert[i] = true 165 | } 166 | // run the step function until no more changes are made 167 | for sa.step(board) { 168 | } 169 | } 170 | 171 | func (sa *StaticAnalyzer) step(board *Board) bool { 172 | changed := false 173 | w := board.Width 174 | h := board.Height 175 | pieces := board.Pieces 176 | // iterate over rows 177 | for y := 0; y < h; y++ { 178 | // find all pieces in this row 179 | positions, sizes := sa.positions[:0], sa.sizes[:0] 180 | for _, piece := range pieces { 181 | if piece.Orientation == Horizontal && piece.Row(w) == y { 182 | positions = append(positions, piece.Col(w)) 183 | sizes = append(sizes, piece.Size) 184 | } 185 | } 186 | // abort early if row is empty 187 | if len(positions) == 0 { 188 | continue 189 | } 190 | // figure out which squares are blocked from opposite direction 191 | blocked := sa.blocked[:0] 192 | i0 := y * w 193 | for i := 0; i < w; i++ { 194 | if sa.horz[i0+i] { 195 | blocked = append(blocked, i) 196 | } 197 | } 198 | // update blocked squares on this row 199 | result := sa.blockedSquares(w, positions, sizes, blocked) 200 | for _, i := range result { 201 | i = i + i0 202 | if !sa.vert[i] { 203 | sa.vert[i] = true 204 | changed = true 205 | } 206 | } 207 | } 208 | // iterate over cols 209 | for x := 0; x < w; x++ { 210 | // find all pieces in this col 211 | positions, sizes := sa.positions[:0], sa.sizes[:0] 212 | for _, piece := range pieces { 213 | if piece.Orientation == Vertical && piece.Col(w) == x { 214 | positions = append(positions, piece.Row(w)) 215 | sizes = append(sizes, piece.Size) 216 | } 217 | } 218 | // abort early if col is empty 219 | if len(positions) == 0 { 220 | continue 221 | } 222 | // figure out which squares are blocked from opposite direction 223 | blocked := sa.blocked[:0] 224 | i0 := x 225 | for i := 0; i < h; i++ { 226 | if sa.vert[i0+i*w] { 227 | blocked = append(blocked, i) 228 | } 229 | } 230 | // update blocked squares on this col 231 | result := sa.blockedSquares(h, positions, sizes, blocked) 232 | for _, i := range result { 233 | i = i*w + x 234 | if !sa.horz[i] { 235 | sa.horz[i] = true 236 | changed = true 237 | } 238 | } 239 | } 240 | // return true if any changes were made 241 | return changed 242 | } 243 | 244 | func (sa *StaticAnalyzer) blockedSquares(w int, positions, sizes, blocked []int) []int { 245 | n := len(positions) 246 | // insertion sort the positions & sizes together 247 | for i := 1; i < n; i++ { 248 | for j := i; j > 0 && positions[j] < positions[j-1]; j-- { 249 | positions[j], positions[j-1] = positions[j-1], positions[j] 250 | sizes[j], sizes[j-1] = sizes[j-1], sizes[j] 251 | } 252 | } 253 | // for each piece, determine its possible placements based on w and blocked 254 | placements := sa.placements 255 | lens := sa.lens[:n] 256 | for i := 0; i < n; i++ { 257 | p := positions[i] 258 | s := sizes[i] 259 | // init placement range to the full row 260 | x0 := 0 261 | x1 := w - s 262 | // reduce placement range based on surrounding blocked squares 263 | for _, b := range blocked { 264 | if b < p { 265 | x0 = maxInt(x0, b+1) 266 | } 267 | if b > p { 268 | x1 = minInt(x1, b-s) 269 | } 270 | } 271 | // make a list of all possible piece positions 272 | d := x1 - x0 + 1 273 | for j := 0; j < d; j++ { 274 | placements[i][j] = x0 + j 275 | } 276 | lens[i] = d 277 | } 278 | // do something like itertools.product in python to examine all possible 279 | // placements of all the pieces together 280 | count := 0 281 | // zero out reused buffers 282 | counts := sa.counts[:w] 283 | idx := sa.idx[:n] 284 | for i := range counts { 285 | counts[i] = 0 286 | } 287 | for i := range idx { 288 | idx[i] = 0 289 | } 290 | for { 291 | // make sure pieces aren't overlapping 292 | ok := true 293 | for i := 1; i < n; i++ { 294 | j := i - 1 295 | if placements[i][idx[i]]-placements[j][idx[j]] < sizes[j] { 296 | ok = false 297 | break 298 | } 299 | } 300 | if ok { 301 | // increment count 302 | count++ 303 | // increment counts for occupied squares 304 | for i := 0; i < n; i++ { 305 | p := placements[i][idx[i]] 306 | s := sizes[i] 307 | for j := 0; j < s; j++ { 308 | counts[p+j]++ 309 | } 310 | } 311 | } 312 | // go to next lexicographic index 313 | i := n - 1 314 | for ; i >= 0 && idx[i] == lens[i]-1; i-- { 315 | idx[i] = 0 316 | } 317 | if i < 0 { 318 | break 319 | } 320 | idx[i]++ 321 | } 322 | // see which squares were always occupied 323 | result := sa.result[:0] 324 | for i, n := range counts { 325 | if n == count { 326 | result = append(result, i) 327 | } 328 | } 329 | return result 330 | } 331 | -------------------------------------------------------------------------------- /static_test.go: -------------------------------------------------------------------------------- 1 | package rush 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | ) 7 | 8 | func TestBlockedSquares(t *testing.T) { 9 | test := func(w int, positions, sizes, blocked, expected []int) { 10 | result := theStaticAnalyzer.blockedSquares(w, positions, sizes, blocked) 11 | if !reflect.DeepEqual(result, expected) { 12 | t.Fail() 13 | } 14 | } 15 | 16 | // ..xAA. => ....x. 17 | test(6, []int{3}, []int{2}, []int{2}, []int{4}) 18 | 19 | // AAA.BB => .xx.x. 20 | test(6, []int{0, 4}, []int{3, 2}, []int{}, []int{1, 2, 4}) 21 | 22 | // AA..BB => ...... 23 | test(6, []int{0, 4}, []int{2, 2}, []int{}, []int{}) 24 | 25 | // .x.AA. => ...... 26 | test(6, []int{3}, []int{2}, []int{1}, []int{}) 27 | 28 | // .xAA..BBx.. => ........... 29 | test(11, []int{2, 6}, []int{2, 2}, []int{1, 8}, []int{}) 30 | 31 | // .xAAA.BBx.. => ...xx.x.... 32 | test(11, []int{2, 6}, []int{3, 2}, []int{1, 8}, []int{3, 4, 6}) 33 | } 34 | -------------------------------------------------------------------------------- /unsolver.go: -------------------------------------------------------------------------------- 1 | package rush 2 | 3 | type Unsolver struct { 4 | board *Board 5 | solver *Solver 6 | memo *Memo 7 | bestBoard *Board 8 | bestSolution Solution 9 | } 10 | 11 | func NewUnsolverWithStaticAnalyzer(board *Board, sa *StaticAnalyzer) *Unsolver { 12 | board = board.Copy() 13 | u := Unsolver{} 14 | u.board = board 15 | u.solver = NewSolverWithStaticAnalyzer(board, sa) 16 | u.memo = NewMemo() 17 | return &u 18 | } 19 | 20 | func NewUnsolver(board *Board) *Unsolver { 21 | return NewUnsolverWithStaticAnalyzer(board, theStaticAnalyzer) 22 | } 23 | 24 | func (u *Unsolver) search(previousPiece int) { 25 | board := u.board 26 | 27 | if !u.memo.Add(board.MemoKey(), 0) { 28 | return 29 | } 30 | 31 | solution := u.solver.UnsafeSolve() 32 | 33 | better := false 34 | dNumMoves := solution.NumMoves - u.bestSolution.NumMoves 35 | dNumSteps := solution.NumSteps - u.bestSolution.NumSteps 36 | if dNumMoves >= 0 { 37 | if dNumMoves > 0 { 38 | better = true 39 | } else if dNumMoves == 0 && dNumSteps > 0 { 40 | better = true 41 | } else if dNumMoves == 0 && dNumSteps == 0 && board.MemoKey().Less(u.bestBoard.MemoKey(), true) { 42 | better = true 43 | } 44 | } 45 | 46 | if better { 47 | u.bestSolution = solution 48 | u.bestBoard = board.Copy() 49 | } 50 | 51 | for _, move := range board.Moves(nil) { 52 | if move.Piece == previousPiece { 53 | continue 54 | } 55 | board.DoMove(move) 56 | u.search(move.Piece) 57 | board.UndoMove(move) 58 | } 59 | } 60 | 61 | func (u *Unsolver) unsolve(skipChecks bool) (*Board, Solution) { 62 | u.bestBoard = u.board.Copy() 63 | u.bestSolution = u.solver.solve(skipChecks) 64 | if u.bestSolution.Solvable { 65 | u.search(-1) 66 | } 67 | return u.bestBoard, u.bestSolution 68 | } 69 | 70 | func (u *Unsolver) Unsolve() (*Board, Solution) { 71 | return u.unsolve(false) 72 | } 73 | 74 | func (u *Unsolver) UnsafeUnsolve() (*Board, Solution) { 75 | return u.unsolve(true) 76 | } 77 | -------------------------------------------------------------------------------- /util.go: -------------------------------------------------------------------------------- 1 | package rush 2 | 3 | func minInt(a, b int) int { 4 | if a < b { 5 | return a 6 | } 7 | return b 8 | } 9 | 10 | func maxInt(a, b int) int { 11 | if a > b { 12 | return a 13 | } 14 | return b 15 | } 16 | -------------------------------------------------------------------------------- /web/app.js: -------------------------------------------------------------------------------- 1 | // Constants 2 | 3 | var UnusableHeight = 72 + 24 * 3; 4 | 5 | // Piece 6 | 7 | function Piece(position, size, stride) { 8 | this.position = position; 9 | this.size = size; 10 | this.stride = stride; 11 | this.fixed = size === 1; 12 | } 13 | 14 | Piece.prototype.move = function(steps) { 15 | this.position += this.stride * steps; 16 | } 17 | 18 | Piece.prototype.draw = function(p5, boardSize, offset) { 19 | offset = offset || 0; 20 | var i0 = this.position; 21 | var i1 = i0 + this.stride * (this.size - 1); 22 | var x0 = Math.floor(i0 % boardSize); 23 | var y0 = Math.floor(i0 / boardSize); 24 | var x1 = Math.floor(i1 % boardSize); 25 | var y1 = Math.floor(i1 / boardSize); 26 | var p = 0.1; 27 | var x = x0 + p; 28 | var y = y0 + p; 29 | var w = x1 - x0 + 1 - p * 2; 30 | var h = y1 - y0 + 1 - p * 2; 31 | if (this.stride === 1) { 32 | x += offset; 33 | } else { 34 | y += offset; 35 | } 36 | p5.rect(x, y, w, h, 0.1); 37 | } 38 | 39 | Piece.prototype.pickAxis = function(point) { 40 | if (this.stride === 1) { 41 | return point.x; 42 | } else { 43 | return point.y; 44 | } 45 | } 46 | 47 | // Move 48 | 49 | function Move(piece, steps) { 50 | this.piece = piece; 51 | this.steps = steps; 52 | } 53 | 54 | // Board 55 | 56 | function Board(desc) { 57 | this.pieces = []; 58 | 59 | // determine board size 60 | this.size = Math.floor(Math.sqrt(desc.length)); 61 | if (this.size === 0) { 62 | throw "board cannot be empty"; 63 | } 64 | 65 | this.size2 = this.size * this.size; 66 | if (this.size2 !== desc.length) { 67 | throw "boards must be square"; 68 | } 69 | 70 | // parse string 71 | var positions = new Map(); 72 | for (var i = 0; i < desc.length; i++) { 73 | var label = desc.charAt(i); 74 | if (!positions.has(label)) { 75 | positions.set(label, []); 76 | } 77 | positions.get(label).push(i); 78 | } 79 | 80 | // sort piece labels 81 | var labels = Array.from(positions.keys()); 82 | labels.sort(); 83 | 84 | // add pieces 85 | for (var label of labels) { 86 | if (label === '.' || label === 'o') { 87 | continue; 88 | } 89 | if (label === 'x') { 90 | continue; 91 | } 92 | var ps = positions.get(label); 93 | if (ps.length < 2) { 94 | throw "piece size must be >= 2"; 95 | } 96 | var stride = ps[1] - ps[0]; 97 | if (stride !== 1 && stride !== this.size) { 98 | throw "invalid piece shape"; 99 | } 100 | for (var i = 2; i < ps.length; i++) { 101 | if (ps[i] - ps[i-1] !== stride) { 102 | throw "invalid piece shape"; 103 | } 104 | } 105 | var piece = new Piece(ps[0], ps.length, stride); 106 | this.addPiece(piece); 107 | } 108 | 109 | // add walls 110 | if (positions.has('x')) { 111 | var ps = positions.get('x'); 112 | for (var p of ps) { 113 | var piece = new Piece(p, 1, 1); 114 | this.addPiece(piece); 115 | } 116 | } 117 | 118 | // compute some stuff 119 | this.primaryRow = 0; 120 | if (this.pieces.length !== 0) { 121 | this.primaryRow = Math.floor(this.pieces[0].position / this.size); 122 | } 123 | } 124 | 125 | Board.prototype.addPiece = function(piece) { 126 | this.pieces.push(piece); 127 | } 128 | 129 | Board.prototype.doMove = function(move) { 130 | this.pieces[move.piece].move(move.steps); 131 | } 132 | 133 | Board.prototype.undoMove = function(move) { 134 | this.pieces[move.piece].move(-move.steps); 135 | } 136 | 137 | Board.prototype.isSolved = function() { 138 | if (this.pieces.length === 0) { 139 | return false; 140 | } 141 | var piece = this.pieces[0]; 142 | var x = Math.floor(piece.position % this.size); 143 | return x + piece.size === this.size; 144 | } 145 | 146 | Board.prototype.pieceAt = function(index) { 147 | for (var i = 0; i < this.pieces.length; i++) { 148 | var piece = this.pieces[i]; 149 | var p = piece.position; 150 | for (var j = 0; j < piece.size; j++) { 151 | if (p === index) { 152 | return i; 153 | } 154 | p += piece.stride; 155 | } 156 | } 157 | return -1; 158 | } 159 | 160 | Board.prototype.isOccupied = function(index) { 161 | return this.pieceAt(index) >= 0; 162 | } 163 | 164 | Board.prototype.moves = function() { 165 | var moves = []; 166 | var size = this.size; 167 | for (var i = 0; i < this.pieces.length; i++) { 168 | var piece = this.pieces[i]; 169 | if (piece.fixed) { 170 | continue; 171 | } 172 | var reverseSteps; 173 | var forwardSteps; 174 | if (piece.stride == 1) { 175 | var x = Math.floor(piece.position % size); 176 | reverseSteps = -x; 177 | forwardSteps = size - piece.size - x; 178 | } else { 179 | var y = Math.floor(piece.position / size); 180 | reverseSteps = -y; 181 | forwardSteps = size - piece.size - y; 182 | } 183 | var idx = piece.position - piece.stride; 184 | for (var steps = -1; steps >= reverseSteps; steps--) { 185 | if (this.isOccupied(idx)) { 186 | break; 187 | } 188 | moves.push(new Move(i, steps)); 189 | idx -= piece.stride; 190 | } 191 | idx = piece.position + piece.size * piece.stride; 192 | for (var steps = 1; steps <= forwardSteps; steps++) { 193 | if (this.isOccupied(idx)) { 194 | break; 195 | } 196 | moves.push(new Move(i, steps)); 197 | idx += piece.stride; 198 | } 199 | } 200 | return moves; 201 | } 202 | 203 | // View 204 | 205 | function View() { 206 | this.board = new Board("IBBxooIooLDDJAALooJoKEEMFFKooMGGHHHM"); 207 | this.movesRequired = 60; 208 | this.dragPiece = -1; 209 | this.dragAnchor = null; 210 | this.dragDelta = null; 211 | this.dragMin = 0; 212 | this.dragMax = 0; 213 | this.undoStack = []; 214 | 215 | this.backgroundColor = "#FFFFFF"; 216 | this.boardColor = "#F2EACD"; 217 | this.gridLineColor = "#222222"; 218 | this.primaryPieceColor = "#CC3333"; 219 | this.pieceColor = "#338899"; 220 | this.pieceOutlineColor = "#222222"; 221 | this.wallColor = "#222222"; 222 | this.wallBoltColor = "#AAAAAA"; 223 | } 224 | 225 | View.prototype.bind = function(p5) { 226 | this.p5 = p5; 227 | } 228 | 229 | View.prototype.setBoard = function(board, movesRequired) { 230 | this.board = board; 231 | this.movesRequired = movesRequired || -1; 232 | this.undoStack = []; 233 | this.changed(); 234 | } 235 | 236 | View.prototype.parseHash = function() { 237 | try { 238 | var hash = location.hash.substring(1); 239 | var i = hash.indexOf('/'); 240 | if (i < 0) { 241 | var desc = hash; 242 | this.setBoard(new Board(desc)); 243 | } else { 244 | var desc = hash.substring(0, i); 245 | var movesRequired = parseInt(hash.substring(i+1)); 246 | this.setBoard(new Board(desc), movesRequired); 247 | } 248 | } 249 | catch (e) { 250 | this.setBoard(new Board("IBBxooIooLDDJAALooJoKEEMFFKooMGGHHHM"), 60); 251 | } 252 | } 253 | 254 | View.prototype.computeScale = function() { 255 | var p5 = this.p5; 256 | var board = this.board; 257 | var xscale = (p5.width / board.size) * 0.9; 258 | var yscale = (p5.height / board.size) * 0.99; 259 | return Math.min(xscale, yscale); 260 | }; 261 | 262 | View.prototype.mouseVector = function() { 263 | var p5 = this.p5; 264 | var board = this.board; 265 | var mx = p5.mouseX || p5.touchX; 266 | var my = p5.mouseY || p5.touchY; 267 | var scale = this.computeScale(); 268 | var x = (mx - p5.width / 2) / scale + board.size / 2; 269 | var y = (my - p5.height / 2) / scale + board.size / 2; 270 | return p5.createVector(x, y); 271 | }; 272 | 273 | View.prototype.mouseIndex = function() { 274 | var p5 = this.p5; 275 | var board = this.board; 276 | var p = this.mouseVector(); 277 | var x = Math.floor(p.x); 278 | var y = Math.floor(p.y); 279 | return y * board.size + x; 280 | }; 281 | 282 | View.prototype.mousePressed = function() { 283 | var p5 = this.p5; 284 | var board = this.board; 285 | this.dragAnchor = this.mouseVector(); 286 | this.dragDelta = p5.createVector(0, 0); 287 | this.dragPiece = board.pieceAt(this.mouseIndex()); 288 | if (this.dragPiece < 0) { 289 | return; 290 | } 291 | var piece = board.pieces[this.dragPiece]; 292 | // can't move walls 293 | if (piece.fixed) { 294 | this.dragPiece = -1; 295 | return; 296 | } 297 | // determine max range 298 | this.dragMin = 0; 299 | this.dragMax = 0; 300 | for (var move of board.moves()) { 301 | if (move.piece === this.dragPiece) { 302 | this.dragMin = Math.min(this.dragMin, move.steps); 303 | this.dragMax = Math.max(this.dragMax, move.steps); 304 | } 305 | } 306 | }; 307 | 308 | View.prototype.mouseReleased = function() { 309 | var p5 = this.p5; 310 | var board = this.board; 311 | if (this.dragPiece < 0) { 312 | return; 313 | } 314 | this.dragDelta = p5.Vector.sub(this.mouseVector(), this.dragAnchor); 315 | var piece = board.pieces[this.dragPiece]; 316 | var steps = Math.round(piece.pickAxis(this.dragDelta)); 317 | steps = Math.min(steps, this.dragMax); 318 | steps = Math.max(steps, this.dragMin); 319 | for (var move of board.moves()) { 320 | if (move.piece === this.dragPiece && move.steps === steps) { 321 | board.doMove(move); 322 | this.undoStack.push(move); 323 | this.changed(); 324 | break; 325 | } 326 | } 327 | this.dragPiece = -1; 328 | }; 329 | 330 | View.prototype.mouseDragged = function() { 331 | var p5 = this.p5; 332 | if (this.dragPiece < 0) { 333 | return; 334 | } 335 | this.dragDelta = p5.Vector.sub(this.mouseVector(), this.dragAnchor); 336 | }; 337 | 338 | View.prototype.touchStarted = function() { 339 | this.mousePressed(); 340 | return false; 341 | }; 342 | 343 | View.prototype.touchEnded = function() { 344 | this.mouseReleased(); 345 | return false; 346 | }; 347 | 348 | View.prototype.touchMoved = function() { 349 | this.mouseDragged(); 350 | return false; 351 | }; 352 | 353 | View.prototype.keyPressed = function() { 354 | var p5 = this.p5; 355 | if (p5.key === 'U') { 356 | this.undo(); 357 | } else if (p5.key === 'R') { 358 | this.reset(); 359 | } 360 | }; 361 | 362 | View.prototype.reset = function() { 363 | var board = this.board; 364 | while (this.undoStack.length > 0) { 365 | var move = this.undoStack.pop(); 366 | board.undoMove(move); 367 | } 368 | this.changed(); 369 | }; 370 | 371 | View.prototype.undo = function() { 372 | var board = this.board; 373 | if (this.undoStack.length > 0) { 374 | var move = this.undoStack.pop(); 375 | board.undoMove(move); 376 | } 377 | this.changed(); 378 | }; 379 | 380 | View.prototype.changed = function() { 381 | $('#numMoves').text(this.undoStack.length); 382 | if (this.movesRequired > 0) { 383 | $('#movesRequired').text('/ ' + this.movesRequired); 384 | } else { 385 | $('#movesRequired').text(''); 386 | } 387 | } 388 | 389 | View.prototype.setup = function() { 390 | var p5 = this.p5; 391 | p5.createCanvas(p5.windowWidth, p5.windowHeight - UnusableHeight); 392 | }; 393 | 394 | View.prototype.windowResized = function() { 395 | var p5 = this.p5; 396 | p5.resizeCanvas(p5.windowWidth, p5.windowHeight - UnusableHeight); 397 | }; 398 | 399 | View.prototype.draw = function() { 400 | var p5 = this.p5; 401 | var board = this.board; 402 | var size = board.size; 403 | 404 | p5.background(this.backgroundColor); 405 | p5.strokeJoin(p5.ROUND); 406 | 407 | var scale = this.computeScale(); 408 | p5.resetMatrix(); 409 | p5.translate(p5.width / 2, p5.height / 2); 410 | p5.scale(scale); 411 | p5.translate(-size / 2, -size / 2); 412 | 413 | // exit 414 | var ex = size; 415 | var ey = board.primaryRow + 0.5; 416 | var es = 0.1; 417 | p5.fill(this.gridLineColor); 418 | p5.noStroke(); 419 | p5.beginShape(); 420 | p5.vertex(ex, ey + es); 421 | p5.vertex(ex, ey - es); 422 | p5.vertex(ex + es, ey); 423 | p5.endShape(p5.CLOSE); 424 | 425 | // board 426 | p5.fill(this.boardColor); 427 | if (board.isSolved()) { 428 | if (Date.now() % 500 < 250) { 429 | p5.fill("#FFFFFF"); 430 | } 431 | } 432 | p5.stroke(this.gridLineColor); 433 | p5.strokeWeight(0.03); 434 | p5.rect(0, 0, size, size, 0.03); 435 | 436 | // walls 437 | p5.noStroke(); 438 | p5.ellipseMode(p5.RADIUS); 439 | for (var piece of board.pieces) { 440 | if (!piece.fixed) { 441 | continue; 442 | } 443 | var x = Math.floor(piece.position % size); 444 | var y = Math.floor(piece.position / size); 445 | p5.fill(this.wallColor); 446 | p5.rect(x, y, 1, 1); 447 | var p = 0.15; 448 | var r = 0.04; 449 | p5.fill(this.wallBoltColor); 450 | p5.ellipse(x + p, y + p, r); 451 | p5.ellipse(x + 1 - p, y + p, r); 452 | p5.ellipse(x + p, y + 1 - p, r); 453 | p5.ellipse(x + 1 - p, y + 1 - p, r); 454 | } 455 | 456 | // grid lines 457 | p5.stroke(this.gridLineColor); 458 | p5.strokeWeight(0.015); 459 | for (var i = 1; i < size; i++) { 460 | p5.line(i, 0, i, size); 461 | p5.line(0, i, size, i); 462 | } 463 | 464 | // pieces 465 | p5.stroke(this.pieceOutlineColor); 466 | p5.strokeWeight(0.03); 467 | for (var i = 0; i < board.pieces.length; i++) { 468 | if (i === this.dragPiece) { 469 | continue; 470 | } 471 | var piece = board.pieces[i]; 472 | if (piece.fixed) { 473 | continue; 474 | } 475 | if (i === 0) { 476 | p5.fill(this.primaryPieceColor); 477 | } else { 478 | p5.fill(this.pieceColor); 479 | } 480 | piece.draw(p5, size); 481 | } 482 | 483 | // dragging 484 | if (this.dragPiece >= 0) { 485 | var piece = board.pieces[this.dragPiece]; 486 | var offset = piece.pickAxis(this.dragDelta); 487 | offset = Math.min(offset, this.dragMax); 488 | offset = Math.max(offset, this.dragMin); 489 | var steps = Math.round(offset); 490 | if (this.dragPiece === 0) { 491 | p5.fill(this.primaryPieceColor); 492 | } else { 493 | p5.fill(this.pieceColor); 494 | } 495 | p5.stroke(this.pieceOutlineColor); 496 | piece.draw(p5, size, offset); 497 | } 498 | }; 499 | 500 | // 501 | 502 | function randomBoard() { 503 | $.getJSON("https://www.michaelfogleman.com/rushserver/random.json", function(data) { 504 | location.hash = data.desc + "/" + data.moves; 505 | }); 506 | } 507 | 508 | var view = new View(); 509 | 510 | var sketch = function(p) { 511 | p.Vector = p5.Vector; 512 | view.bind(p); 513 | p.draw = function() { view.draw(); } 514 | p.keyPressed = function() { view.keyPressed(); } 515 | p.mouseDragged = function() { view.mouseDragged(); } 516 | p.mousePressed = function() { view.mousePressed(); } 517 | p.mouseReleased = function() { view.mouseReleased(); } 518 | p.setup = function() { view.setup(); }; 519 | p.touchEnded = function() { view.touchEnded(); } 520 | p.touchMoved = function() { view.touchMoved(); } 521 | p.touchStarted = function() { view.touchStarted(); } 522 | p.windowResized = function() { view.windowResized(); } 523 | }; 524 | 525 | new p5(sketch, 'view'); 526 | 527 | $(function() { 528 | document.ontouchmove = function(event) { 529 | event.preventDefault(); 530 | } 531 | 532 | window.onhashchange = function() { 533 | view.parseHash(); 534 | } 535 | 536 | $('#resetButton').click(function() { 537 | view.reset(); 538 | }); 539 | 540 | $('#undoButton').click(function() { 541 | view.undo(); 542 | }); 543 | 544 | $('#randomButton').click(function() { 545 | randomBoard(); 546 | }); 547 | 548 | view.parseHash(); 549 | }); 550 | -------------------------------------------------------------------------------- /web/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Rush Hour 8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /web/style.css: -------------------------------------------------------------------------------- 1 | html, body { 2 | height: 100%; 3 | } 4 | 5 | body { 6 | padding: 0; 7 | margin: 0; 8 | position: fixed; 9 | font-family: sans-serif; 10 | color: #333; 11 | overscroll-behavior-y: contain; 12 | } 13 | 14 | #view { 15 | margin-top: 24px; 16 | } 17 | 18 | .footer { 19 | position: absolute; 20 | left: 0; 21 | right: 0; 22 | bottom: 0; 23 | padding: 24px 0; 24 | height: 72px; 25 | text-align: center; 26 | } 27 | 28 | .buttons { 29 | margin-top: 8px; 30 | } 31 | 32 | .label { 33 | font-size: 14px; 34 | line-height: 24px; 35 | text-transform: uppercase; 36 | vertical-align: baseline; 37 | } 38 | 39 | .value { 40 | font-size: 24px; 41 | } 42 | 43 | button { 44 | font-size: 18px; 45 | border-radius: 4px; 46 | padding: 4px 8px; 47 | margin: 0 4px; 48 | width: 96px; 49 | } 50 | 51 | @media (max-width: 512px) { 52 | button { 53 | font-size: 14px; 54 | width: 72px; 55 | } 56 | } 57 | --------------------------------------------------------------------------------