├── screenshots ├── heart.png ├── tetris.png ├── editmode.png └── highscores.png ├── Dockerfile ├── tetris_test.go ├── boards_test.go ├── tetris.go ├── LICENSE ├── colorTest └── main.go ├── ranking.go ├── README.md ├── minos.go ├── globals.go ├── engineKeyInput.go ├── edit.go ├── mino.go ├── ai.go ├── engine.go ├── mino_test.go ├── board.go ├── view.go ├── ai_test.go └── boards.go /screenshots/heart.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MichaelS11/go-tetris/HEAD/screenshots/heart.png -------------------------------------------------------------------------------- /screenshots/tetris.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MichaelS11/go-tetris/HEAD/screenshots/tetris.png -------------------------------------------------------------------------------- /screenshots/editmode.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MichaelS11/go-tetris/HEAD/screenshots/editmode.png -------------------------------------------------------------------------------- /screenshots/highscores.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MichaelS11/go-tetris/HEAD/screenshots/highscores.png -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:alpine as builder 2 | 3 | WORKDIR /go/src/github.com/MichaelS11/go-tetris 4 | COPY . . 5 | 6 | RUN apk add --no-cache git 7 | RUN go get ./ 8 | RUN go build -ldflags="-extldflags=-static" -o /go/bin/go-tetris 9 | 10 | FROM scratch 11 | WORKDIR / 12 | COPY --from=builder /go/bin/go-tetris . 13 | 14 | ENTRYPOINT ["./go-tetris"] 15 | -------------------------------------------------------------------------------- /tetris_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "math/rand" 6 | "os" 7 | "testing" 8 | 9 | "github.com/gdamore/tcell" 10 | ) 11 | 12 | type testMinoStruct struct { 13 | minoRotation MinoRotation 14 | x int 15 | y int 16 | } 17 | 18 | func TestMain(m *testing.M) { 19 | setupForTesting() 20 | retCode := m.Run() 21 | os.Exit(retCode) 22 | } 23 | 24 | func setupForTesting() { 25 | logger = log.New(os.Stdout, "", log.Ldate|log.Ltime|log.LUTC|log.Lshortfile) 26 | 27 | rand.Seed(1) 28 | 29 | err := loadBoards() 30 | if err != nil { 31 | log.Fatal("error loading boards:", err) 32 | } 33 | 34 | screen, err = tcell.NewScreen() 35 | if err != nil { 36 | logger.Fatal("NewScreen error:", err) 37 | } 38 | 39 | NewMinos() 40 | NewBoard() 41 | NewEngine() 42 | } 43 | -------------------------------------------------------------------------------- /boards_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestBoards(t *testing.T) { 8 | for i := 0; i < len(boards); i++ { 9 | b := boards[i] 10 | width := len(b.colors) 11 | height := len(b.colors[0]) 12 | 13 | for j := 1; j < width; j++ { 14 | if len(b.colors[j]) != height { 15 | t.Fatalf("board height - received: %v - expected: %v - index %v", len(b.colors[j]), height, i) 16 | } 17 | } 18 | 19 | if len(b.rotation) != width { 20 | t.Fatalf("rotation width - received: %v - expected: %v - index %v", len(b.rotation), width, i) 21 | } 22 | 23 | for j := 0; j < width; j++ { 24 | if len(b.rotation[j]) != height { 25 | t.Fatalf("rotation height - received: %v - expected: %v - index %v", len(b.rotation[j]), height, i) 26 | } 27 | } 28 | 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /tetris.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "math/rand" 6 | "os" 7 | "path/filepath" 8 | "time" 9 | ) 10 | 11 | func main() { 12 | baseDir, _ = filepath.Abs(filepath.Dir(os.Args[0])) 13 | logger = log.New(os.Stderr, "", log.Ldate|log.Ltime|log.LUTC|log.Lshortfile) 14 | logFile, err := os.OpenFile(baseDir+"/go-tetris.log", os.O_RDWR|os.O_CREATE|os.O_APPEND, 0644) 15 | if err != nil { 16 | logger.Fatal("opening log file error:", err) 17 | } 18 | defer logFile.Close() 19 | logger.SetOutput(logFile) 20 | 21 | rand.Seed(time.Now().UnixNano()) 22 | 23 | err = loadBoards() 24 | if err != nil { 25 | logger.Fatal("loading internal boards error:", err) 26 | } 27 | 28 | err = loadUserBoards() 29 | if err != nil { 30 | logger.Fatal("loading user boards error:", err) 31 | } 32 | 33 | NewView() 34 | NewMinos() 35 | NewBoard() 36 | NewEdit() 37 | NewEngine() 38 | 39 | engine.Start() 40 | 41 | view.Stop() 42 | } 43 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Original work Copyright (c) 2014 Takashi Kokubun 4 | Modified work Copyright (c) 2017 MichaelS11 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | -------------------------------------------------------------------------------- /colorTest/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "strconv" 6 | 7 | "github.com/gdamore/tcell" 8 | ) 9 | 10 | var ( 11 | screen tcell.Screen 12 | x = 0 13 | y = 1 14 | ) 15 | 16 | func main() { 17 | var err error 18 | screen, err = tcell.NewScreen() 19 | if err != nil { 20 | fmt.Println("NewScreen error:", err) 21 | } 22 | err = screen.Init() 23 | if err != nil { 24 | fmt.Println("screen Init error:", err) 25 | } 26 | 27 | screen.Clear() 28 | 29 | for i := 0; i < 379; i++ { 30 | printNum(i) 31 | style := tcell.StyleDefault.Foreground(tcell.Color(i)).Background(tcell.Color(i)).Dim(true) 32 | screen.SetContent(x, y, '▄', nil, style) 33 | x++ 34 | screen.SetContent(x, y, '▄', nil, style) 35 | x += 2 36 | if x > 80 { 37 | x = 0 38 | y += 2 39 | } 40 | if i == 15 { 41 | i = 255 42 | } 43 | } 44 | 45 | screen.Show() 46 | } 47 | 48 | func printNum(num int) { 49 | word := strconv.FormatInt(int64(num), 10) + ":" 50 | if num < 10 { 51 | word = " " + word 52 | } else if num < 100 { 53 | word = " " + word 54 | } 55 | if len(word)+x+2 > 80 { 56 | x = 0 57 | y += 2 58 | } 59 | style := tcell.StyleDefault.Foreground(tcell.ColorLightGray).Background(tcell.ColorBlack) 60 | for _, char := range word { 61 | screen.SetContent(x, y, char, nil, style) 62 | x++ 63 | } 64 | 65 | } 66 | -------------------------------------------------------------------------------- /ranking.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "io/ioutil" 6 | "os" 7 | "strconv" 8 | "strings" 9 | ) 10 | 11 | // NewRanking create a new ranking 12 | func NewRanking() *Ranking { 13 | ranking := &Ranking{ 14 | scores: make([]uint64, 9), 15 | } 16 | 17 | if _, err := os.Stat(baseDir + rankingFileName); os.IsNotExist(err) { 18 | for i := 0; i < 9; i++ { 19 | ranking.scores[i] = 0 20 | } 21 | return ranking 22 | } 23 | 24 | scoreBytes, err := ioutil.ReadFile(baseDir + rankingFileName) 25 | if err != nil { 26 | logger.Println("NewRanking ReadFile error:", err) 27 | } 28 | 29 | scoreStrings := strings.Split(string(scoreBytes), ",") 30 | for index, scoreString := range scoreStrings { 31 | if index > 8 { 32 | break 33 | } 34 | score, err := strconv.ParseUint(scoreString, 10, 64) 35 | if err != nil { 36 | logger.Println("NewRanking ParseUint error:", err) 37 | score = 0 38 | } 39 | ranking.scores[index] = score 40 | } 41 | 42 | return ranking 43 | } 44 | 45 | // Save saves the rankings to a file 46 | func (ranking *Ranking) Save() { 47 | var buffer bytes.Buffer 48 | 49 | for i := 0; i < 9; i++ { 50 | if i != 0 { 51 | buffer.WriteRune(',') 52 | } 53 | buffer.WriteString(strconv.FormatUint(ranking.scores[i], 10)) 54 | } 55 | 56 | ioutil.WriteFile(baseDir+rankingFileName, buffer.Bytes(), 0644) 57 | } 58 | 59 | // InsertScore inserts a score into the rankings 60 | func (ranking *Ranking) InsertScore(newScore uint64) { 61 | for index, score := range ranking.scores { 62 | if newScore > score { 63 | ranking.slideScores(index) 64 | ranking.scores[index] = newScore 65 | return 66 | } 67 | } 68 | } 69 | 70 | // slideScores slides the scores down to make room for a new score 71 | func (ranking *Ranking) slideScores(index int) { 72 | for i := len(ranking.scores) - 1; i > index; i-- { 73 | ranking.scores[i] = ranking.scores[i-1] 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Go Tetris 2 | 3 | Golang Tetris for console window with optional AI 4 | 5 | ## Features include 6 | 7 | - AI (use i key to toggle) 8 | - Lock delay 9 | - Next piece 10 | - Ghost piece 11 | - Top scores 12 | - Board choices 13 | - Edit boards 14 | 15 | ## Compile 16 | 17 | ``` 18 | go get github.com/MichaelS11/go-tetris 19 | go install github.com/MichaelS11/go-tetris 20 | ``` 21 | 22 | ## Play 23 | 24 | Then run the binary created, go-tetris or go-tetris.exe 25 | 26 | ## Keys start screen 27 | 28 | | Key | Action | 29 | | --- | --- | 30 | | ← | previous board | 31 | | → | next board | 32 | | spacebar | start game | 33 | | ctrl e | edit board | 34 | | q | quit | 35 | 36 | ## Keys during game 37 | 38 | | Key | Action | 39 | | --- | --- | 40 | | ← | left move | 41 | | → | right move | 42 | | ↓ | soft drop | 43 | | ↑ | hard drop | 44 | | spacebar | hard drop | 45 | | z | left rotate | 46 | | x | right rotate | 47 | | p | pause | 48 | | q | quit | 49 | | i | toggle AI | 50 | 51 | ## Keys edit mode 52 | 53 | | Key | Action | 54 | | --- | --- | 55 | | ← | move cursor left | 56 | | → | move cursor right | 57 | | ↓ | move cursor down | 58 | | ↑ | move cursor up | 59 | | z | rotate left | 60 | | x | rotate right | 61 | | c | cyan block - I | 62 | | b | blue block - J | 63 | | w | white block - L | 64 | | e | yellow block - O | 65 | | g | green block - S | 66 | | a | magenta block - T | 67 | | r | red block - Z | 68 | | f | free block | 69 | | ctrl b | change board size | 70 | | ctrl s | save board | 71 | | ctrl n | save board as new | 72 | | ctrl k | delete board | 73 | | ctrl o | empty board | 74 | | ctrl q | quit edit mode | 75 | 76 | ## Screenshots 77 | 78 | ![alt text](https://raw.githubusercontent.com/MichaelS11/tetris/master/screenshots/tetris.png "Go Tetris") 79 | 80 | ![alt text](https://raw.githubusercontent.com/MichaelS11/tetris/master/screenshots/heart.png "Golang Tetris Heart") 81 | 82 | ![alt text](https://raw.githubusercontent.com/MichaelS11/tetris/master/screenshots/editmode.png "Edit Mode Peace Symbol") 83 | 84 | ![alt text](https://raw.githubusercontent.com/MichaelS11/tetris/master/screenshots/highscores.png "Tetris High Scores") 85 | 86 | ## To do 87 | 88 | * Improve AI speed (slow on large boards) 89 | -------------------------------------------------------------------------------- /minos.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "math/rand" 5 | 6 | "github.com/gdamore/tcell" 7 | ) 8 | 9 | // NewMinos creates the minos and minoBag 10 | func NewMinos() { 11 | minoI := MinoBlocks{ 12 | []tcell.Color{colorBlank, colorCyan, colorBlank, colorBlank}, 13 | []tcell.Color{colorBlank, colorCyan, colorBlank, colorBlank}, 14 | []tcell.Color{colorBlank, colorCyan, colorBlank, colorBlank}, 15 | []tcell.Color{colorBlank, colorCyan, colorBlank, colorBlank}, 16 | } 17 | minoJ := MinoBlocks{ 18 | []tcell.Color{colorBlue, colorBlue, colorBlank}, 19 | []tcell.Color{colorBlank, colorBlue, colorBlank}, 20 | []tcell.Color{colorBlank, colorBlue, colorBlank}, 21 | } 22 | minoL := MinoBlocks{ 23 | []tcell.Color{colorBlank, colorWhite, colorBlank}, 24 | []tcell.Color{colorBlank, colorWhite, colorBlank}, 25 | []tcell.Color{colorWhite, colorWhite, colorBlank}, 26 | } 27 | minoO := MinoBlocks{ 28 | []tcell.Color{colorYellow, colorYellow}, 29 | []tcell.Color{colorYellow, colorYellow}, 30 | } 31 | minoS := MinoBlocks{ 32 | []tcell.Color{colorBlank, colorGreen, colorBlank}, 33 | []tcell.Color{colorGreen, colorGreen, colorBlank}, 34 | []tcell.Color{colorGreen, colorBlank, colorBlank}, 35 | } 36 | minoT := MinoBlocks{ 37 | []tcell.Color{colorBlank, colorMagenta, colorBlank}, 38 | []tcell.Color{colorMagenta, colorMagenta, colorBlank}, 39 | []tcell.Color{colorBlank, colorMagenta, colorBlank}, 40 | } 41 | minoZ := MinoBlocks{ 42 | []tcell.Color{colorRed, colorBlank, colorBlank}, 43 | []tcell.Color{colorRed, colorRed, colorBlank}, 44 | []tcell.Color{colorBlank, colorRed, colorBlank}, 45 | } 46 | 47 | var minoRotationI MinoRotation 48 | minoRotationI[0] = minoI 49 | for i := 1; i < 4; i++ { 50 | minoRotationI[i] = minosCloneRotateRight(minoRotationI[i-1]) 51 | } 52 | var minoRotationJ MinoRotation 53 | minoRotationJ[0] = minoJ 54 | for i := 1; i < 4; i++ { 55 | minoRotationJ[i] = minosCloneRotateRight(minoRotationJ[i-1]) 56 | } 57 | var minoRotationL MinoRotation 58 | minoRotationL[0] = minoL 59 | for i := 1; i < 4; i++ { 60 | minoRotationL[i] = minosCloneRotateRight(minoRotationL[i-1]) 61 | } 62 | var minoRotationO MinoRotation 63 | minoRotationO[0] = minoO 64 | minoRotationO[1] = minoO 65 | minoRotationO[2] = minoO 66 | minoRotationO[3] = minoO 67 | var minoRotationS MinoRotation 68 | minoRotationS[0] = minoS 69 | for i := 1; i < 4; i++ { 70 | minoRotationS[i] = minosCloneRotateRight(minoRotationS[i-1]) 71 | } 72 | var minoRotationT MinoRotation 73 | minoRotationT[0] = minoT 74 | for i := 1; i < 4; i++ { 75 | minoRotationT[i] = minosCloneRotateRight(minoRotationT[i-1]) 76 | } 77 | var minoRotationZ MinoRotation 78 | minoRotationZ[0] = minoZ 79 | for i := 1; i < 4; i++ { 80 | minoRotationZ[i] = minosCloneRotateRight(minoRotationZ[i-1]) 81 | } 82 | 83 | minos = &Minos{ 84 | minoBag: [7]MinoRotation{minoRotationI, minoRotationJ, minoRotationL, minoRotationO, minoRotationS, minoRotationT, minoRotationZ}, 85 | bagRand: rand.Perm(7), 86 | } 87 | } 88 | 89 | // minosCloneRotateRight clones a mino and rotates the mino to the right 90 | func minosCloneRotateRight(minoBlocks MinoBlocks) MinoBlocks { 91 | length := len(minoBlocks) 92 | newMinoBlocks := make(MinoBlocks, length, length) 93 | for i := 0; i < length; i++ { 94 | newMinoBlocks[i] = make([]tcell.Color, length, length) 95 | } 96 | 97 | for i := 0; i < length; i++ { 98 | for j := 0; j < length; j++ { 99 | newMinoBlocks[length-j-1][i] = minoBlocks[i][j] 100 | } 101 | } 102 | 103 | return newMinoBlocks 104 | } 105 | -------------------------------------------------------------------------------- /globals.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "time" 6 | 7 | "github.com/gdamore/tcell" 8 | ) 9 | 10 | const ( 11 | boardXOffset = 4 12 | boardYOffset = 2 13 | aiTickDivider = 8 14 | rankingFileName = "/go-tetris.db" 15 | settingsFileName = "/go-tetris.json" 16 | 17 | // MinoPreview is for the preview mino 18 | MinoPreview MinoType = iota 19 | // MinoCurrent is for the current mino 20 | MinoCurrent = iota 21 | // MinoDrop is for the drop mino 22 | MinoDrop = iota 23 | 24 | colorBlank = tcell.ColorBlack 25 | colorCyan = tcell.ColorAqua // I 26 | colorBlue = tcell.ColorBlue // J 27 | colorWhite = tcell.ColorWhite // L 28 | colorYellow = tcell.ColorYellow // O 29 | colorGreen = tcell.ColorLime // S 30 | colorMagenta = tcell.ColorFuchsia // T 31 | colorRed = tcell.ColorRed // Z 32 | 33 | engineModeRun engineMode = iota 34 | engineModeRunWithAI 35 | engineModeStopped 36 | engineModeGameOver 37 | engineModePaused 38 | engineModePreview 39 | engineModeEdit 40 | ) 41 | 42 | type ( 43 | engineMode int 44 | 45 | // MinoType is the type of mino 46 | MinoType int 47 | // MinoBlocks is the blocks of the mino 48 | MinoBlocks [][]tcell.Color 49 | // MinoRotation is the rotation of the mino 50 | MinoRotation [4]MinoBlocks 51 | 52 | // Mino is a mino 53 | Mino struct { 54 | x int 55 | y int 56 | length int 57 | rotation int 58 | minoRotation MinoRotation 59 | } 60 | 61 | // Minos is a bag of minos 62 | Minos struct { 63 | minoBag [7]MinoRotation 64 | bagRand []int 65 | bagIndex int 66 | } 67 | 68 | // Board is the Tetris board 69 | Board struct { 70 | boardsIndex int 71 | width int 72 | height int 73 | colors [][]tcell.Color 74 | rotation [][]int 75 | previewMino *Mino 76 | currentMino *Mino 77 | dropDistance int 78 | fullLinesY []bool 79 | } 80 | 81 | // Boards holds all the boards 82 | Boards struct { 83 | name string 84 | colors [][]tcell.Color 85 | rotation [][]int 86 | } 87 | 88 | // BoardsJSON is for JSON format of boards 89 | BoardsJSON struct { 90 | Name string 91 | Mino [][]string 92 | Rotation [][]int 93 | } 94 | 95 | // View is the display engine 96 | View struct { 97 | } 98 | 99 | // Ai is the AI engine 100 | Ai struct { 101 | queue *[]rune 102 | newQueue *[]rune 103 | index int 104 | } 105 | 106 | // Ranking holds the ranking scores 107 | Ranking struct { 108 | scores []uint64 109 | } 110 | 111 | // Engine is the Tetirs game engine 112 | Engine struct { 113 | stopped bool 114 | chanStop chan struct{} 115 | chanEventKey chan *tcell.EventKey 116 | ranking *Ranking 117 | timer *time.Timer 118 | tickTime time.Duration 119 | mode engineMode 120 | score int 121 | level int 122 | deleteLines int 123 | ai *Ai 124 | aiEnabled bool 125 | aiTimer *time.Timer 126 | } 127 | 128 | // Edit is the board edit mode 129 | Edit struct { 130 | x int 131 | y int 132 | moved bool 133 | boardSize bool 134 | width int 135 | height int 136 | saved bool 137 | } 138 | 139 | // Settings is the JSON load/save file 140 | Settings struct { 141 | Boards []BoardsJSON 142 | } 143 | 144 | // EventGame is an game event 145 | EventGame struct { 146 | when time.Time 147 | } 148 | ) 149 | 150 | // When returns event when 151 | func (EventGame *EventGame) When() time.Time { 152 | return EventGame.when 153 | } 154 | 155 | var ( 156 | baseDir string 157 | logger *log.Logger 158 | screen tcell.Screen 159 | minos *Minos 160 | board *Board 161 | view *View 162 | engine *Engine 163 | edit *Edit 164 | 165 | boards []Boards 166 | numInternalBoards int 167 | ) 168 | -------------------------------------------------------------------------------- /engineKeyInput.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "runtime" 5 | 6 | "github.com/gdamore/tcell" 7 | ) 8 | 9 | // ProcessEventKey process the key input event 10 | func (engine *Engine) ProcessEventKey(eventKey *tcell.EventKey) { 11 | if eventKey.Key() == tcell.KeyCtrlL { 12 | // Ctrl l (lower case L) to log stack trace 13 | buffer := make([]byte, 1<<16) 14 | length := runtime.Stack(buffer, true) 15 | logger.Println("Stack trace") 16 | logger.Println(string(buffer[:length])) 17 | return 18 | } 19 | 20 | switch engine.mode { 21 | 22 | // edit 23 | case engineModeEdit: 24 | 25 | if edit.boardSize { 26 | switch eventKey.Key() { 27 | case tcell.KeyUp: 28 | edit.BoardHeightIncrement() 29 | case tcell.KeyDown: 30 | edit.BoardHeightDecrement() 31 | case tcell.KeyLeft: 32 | edit.BoardWidthDecrement() 33 | case tcell.KeyRight: 34 | edit.BoardWidthIncrement() 35 | case tcell.KeyRune: 36 | if eventKey.Rune() == 'q' { 37 | edit.ChangeBoardSize() 38 | } 39 | } 40 | } else { 41 | switch eventKey.Key() { 42 | case tcell.KeyUp: 43 | edit.CursorUp() 44 | case tcell.KeyDown: 45 | edit.CursorDown() 46 | case tcell.KeyLeft: 47 | edit.CursorLeft() 48 | case tcell.KeyRight: 49 | edit.CursorRight() 50 | case tcell.KeyCtrlB: 51 | edit.BoardSizeMode() 52 | case tcell.KeyCtrlS: 53 | edit.SaveBoard() 54 | case tcell.KeyCtrlN: 55 | edit.SaveBoardNew() 56 | case tcell.KeyCtrlK: 57 | edit.DeleteBoard() 58 | case tcell.KeyCtrlO: 59 | edit.EmptyBoard() 60 | case tcell.KeyCtrlQ, tcell.KeyCtrlC: 61 | engine.DisableEditMode() 62 | case tcell.KeyRune: 63 | switch eventKey.Rune() { 64 | case 'c': 65 | edit.SetColor(colorCyan) 66 | case 'b': 67 | edit.SetColor(colorBlue) 68 | case 'w': 69 | edit.SetColor(colorWhite) 70 | case 'e': 71 | edit.SetColor(colorYellow) 72 | case 'g': 73 | edit.SetColor(colorGreen) 74 | case 'a': 75 | edit.SetColor(colorMagenta) 76 | case 'r': 77 | edit.SetColor(colorRed) 78 | case 'f': 79 | edit.SetColor(colorBlank) 80 | case 'z': 81 | edit.RotateLeft() 82 | case 'x': 83 | edit.RotateRight() 84 | } 85 | } 86 | } 87 | 88 | // game over 89 | case engineModeGameOver, engineModePreview: 90 | 91 | switch eventKey.Key() { 92 | case tcell.KeyCtrlC: 93 | engine.Stop() 94 | case tcell.KeyLeft: 95 | board.PreviousBoard() 96 | case tcell.KeyRight: 97 | board.NextBoard() 98 | case tcell.KeyCtrlE: 99 | engine.EnabledEditMode() 100 | case tcell.KeyRune: 101 | switch eventKey.Rune() { 102 | case 'q': 103 | engine.Stop() 104 | case ' ': 105 | engine.NewGame() 106 | } 107 | } 108 | 109 | // paused 110 | case engineModePaused: 111 | 112 | switch eventKey.Rune() { 113 | case 'q': 114 | engine.Stop() 115 | case 'p': 116 | engine.UnPause() 117 | } 118 | 119 | // run with AI 120 | case engineModeRunWithAI: 121 | 122 | switch eventKey.Rune() { 123 | case 'q': 124 | engine.Stop() 125 | case 'p': 126 | engine.Pause() 127 | case 'i': 128 | engine.DisableAi() 129 | } 130 | 131 | // run 132 | case engineModeRun: 133 | 134 | switch eventKey.Key() { 135 | case tcell.KeyUp: 136 | board.MinoDrop() 137 | case tcell.KeyDown: 138 | board.MinoMoveDown() 139 | case tcell.KeyLeft: 140 | board.MinoMoveLeft() 141 | case tcell.KeyRight: 142 | board.MinoMoveRight() 143 | case tcell.KeyRune: 144 | switch eventKey.Rune() { 145 | case 'q': 146 | engine.Stop() 147 | case ' ': 148 | board.MinoDrop() 149 | case 'z': 150 | board.MinoRotateLeft() 151 | case 'x': 152 | board.MinoRotateRight() 153 | case 'p': 154 | engine.Pause() 155 | case 'i': 156 | engine.EnabledAi() 157 | } 158 | } 159 | } 160 | 161 | } 162 | -------------------------------------------------------------------------------- /edit.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/gdamore/tcell" 7 | ) 8 | 9 | // NewEdit creates a new edit mode 10 | func NewEdit() { 11 | edit = &Edit{moved: true} 12 | } 13 | 14 | // EnabledEditMode enable edit mode 15 | func (edit *Edit) EnabledEditMode() { 16 | if edit.y > board.height-1 { 17 | edit.y = board.height - 1 18 | } 19 | if edit.x > board.width-1 { 20 | edit.x = board.width - 1 21 | } 22 | edit.moved = true 23 | } 24 | 25 | // DisableEditMode disable edit mode 26 | func (edit *Edit) DisableEditMode() { 27 | err := saveUserBoards() 28 | if err != nil { 29 | logger.Fatal("error saving user boards:", err) 30 | } 31 | } 32 | 33 | // BoardSizeMode changed to board size edit mode 34 | func (edit *Edit) BoardSizeMode() { 35 | edit.width = board.width 36 | edit.height = board.height 37 | edit.boardSize = true 38 | } 39 | 40 | // BoardWidthIncrement board width increment 41 | func (edit *Edit) BoardWidthIncrement() { 42 | if edit.width > 39 { 43 | return 44 | } 45 | edit.width++ 46 | } 47 | 48 | // BoardWidthDecrement board width decrement 49 | func (edit *Edit) BoardWidthDecrement() { 50 | if edit.width < 9 { 51 | return 52 | } 53 | edit.width-- 54 | } 55 | 56 | // BoardHeightIncrement board height increment 57 | func (edit *Edit) BoardHeightIncrement() { 58 | if edit.height > 39 { 59 | return 60 | } 61 | edit.height++ 62 | } 63 | 64 | // BoardHeightDecrement board height decrement 65 | func (edit *Edit) BoardHeightDecrement() { 66 | if edit.height < 9 { 67 | return 68 | } 69 | edit.height-- 70 | } 71 | 72 | // ChangeBoardSize create new board 73 | func (edit *Edit) ChangeBoardSize() { 74 | ChangeBoardSize(edit.width, edit.height) 75 | edit.saved = false 76 | edit.boardSize = false 77 | } 78 | 79 | // EmptyBoard removes all blocks/colors from the board 80 | func (edit *Edit) EmptyBoard() { 81 | board.EmptyBoard() 82 | } 83 | 84 | // CursorUp move cursor up 85 | func (edit *Edit) CursorUp() { 86 | if !edit.moved { 87 | edit.moved = true 88 | } 89 | if edit.y < 1 { 90 | return 91 | } 92 | edit.y-- 93 | } 94 | 95 | // CursorDown move cursor down 96 | func (edit *Edit) CursorDown() { 97 | if !edit.moved { 98 | edit.moved = true 99 | } 100 | if edit.y == board.height-1 { 101 | return 102 | } 103 | edit.y++ 104 | } 105 | 106 | // CursorRight move cursor right 107 | func (edit *Edit) CursorRight() { 108 | if !edit.moved { 109 | edit.moved = true 110 | } 111 | if edit.x == board.width-1 { 112 | return 113 | } 114 | edit.x++ 115 | } 116 | 117 | // CursorLeft move cursor left 118 | func (edit *Edit) CursorLeft() { 119 | if !edit.moved { 120 | edit.moved = true 121 | } 122 | if edit.x < 1 { 123 | return 124 | } 125 | edit.x-- 126 | } 127 | 128 | // SetColor sets the board color 129 | func (edit *Edit) SetColor(color tcell.Color) { 130 | if edit.moved { 131 | edit.moved = false 132 | } 133 | if edit.saved { 134 | edit.saved = false 135 | } 136 | board.SetColor(edit.x, edit.y, color, -1) 137 | } 138 | 139 | // RotateLeft rotates cell left 140 | func (edit *Edit) RotateLeft() { 141 | if edit.moved { 142 | edit.moved = false 143 | } 144 | if edit.saved { 145 | edit.saved = false 146 | } 147 | board.RotateLeft(edit.x, edit.y) 148 | } 149 | 150 | // RotateRight rotates cell right 151 | func (edit *Edit) RotateRight() { 152 | if edit.moved { 153 | edit.moved = false 154 | } 155 | if edit.saved { 156 | edit.saved = false 157 | } 158 | board.RotateRight(edit.x, edit.y) 159 | } 160 | 161 | // DrawCursor draws the cursor location when cursor moves 162 | func (edit *Edit) DrawCursor() { 163 | if !edit.moved { 164 | return 165 | } 166 | board.DrawCursor(edit.x, edit.y) 167 | } 168 | 169 | // SaveBoard save board 170 | func (edit *Edit) SaveBoard() { 171 | if board.boardsIndex < numInternalBoards { 172 | edit.SaveBoardNew() 173 | return 174 | } 175 | boards[board.boardsIndex].colors = board.colors 176 | boards[board.boardsIndex].rotation = board.rotation 177 | if !edit.saved { 178 | edit.saved = true 179 | } 180 | } 181 | 182 | // SaveBoardNew save board as new board 183 | func (edit *Edit) SaveBoardNew() { 184 | aBoards := Boards{name: time.Now().Format("Jan 2 3:4:5")} 185 | aBoards.colors = make([][]tcell.Color, len(board.colors)) 186 | for i := 0; i < len(board.colors); i++ { 187 | aBoards.colors[i] = append(aBoards.colors[i], board.colors[i]...) 188 | } 189 | aBoards.rotation = make([][]int, len(board.rotation)) 190 | for i := 0; i < len(board.rotation); i++ { 191 | aBoards.rotation[i] = append(aBoards.rotation[i], board.rotation[i]...) 192 | } 193 | boards = append(boards, aBoards) 194 | board.boardsIndex = len(boards) - 1 195 | if !edit.saved { 196 | edit.saved = true 197 | } 198 | } 199 | 200 | // DeleteBoard deletes a board 201 | func (edit *Edit) DeleteBoard() { 202 | if board.boardsIndex < numInternalBoards { 203 | return 204 | } 205 | boards = append(boards[:board.boardsIndex], boards[board.boardsIndex+1:]...) 206 | board.boardsIndex-- 207 | board.Clear() 208 | } 209 | -------------------------------------------------------------------------------- /mino.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "math/rand" 5 | 6 | "github.com/gdamore/tcell" 7 | ) 8 | 9 | // NewMino creates a new Mino 10 | func NewMino() *Mino { 11 | minoRotation := minos.minoBag[minos.bagRand[minos.bagIndex]] 12 | minos.bagIndex++ 13 | if minos.bagIndex > 6 { 14 | minos.bagIndex = 0 15 | minos.bagRand = rand.Perm(7) 16 | } 17 | mino := &Mino{ 18 | minoRotation: minoRotation, 19 | length: len(minoRotation[0]), 20 | } 21 | mino.x = board.width/2 - (mino.length+1)/2 22 | mino.y = -1 23 | return mino 24 | } 25 | 26 | // CloneMoveLeft creates copy of the mino and moves it left 27 | func (mino *Mino) CloneMoveLeft() *Mino { 28 | newMino := *mino 29 | newMino.MoveLeft() 30 | return &newMino 31 | } 32 | 33 | // MoveLeft moves the mino left 34 | func (mino *Mino) MoveLeft() { 35 | mino.x-- 36 | } 37 | 38 | // CloneMoveRight creates copy of the mino and moves it right 39 | func (mino *Mino) CloneMoveRight() *Mino { 40 | newMino := *mino 41 | newMino.MoveRight() 42 | return &newMino 43 | } 44 | 45 | // MoveRight moves the mino right 46 | func (mino *Mino) MoveRight() { 47 | mino.x++ 48 | } 49 | 50 | // CloneRotateRight creates copy of the mino and rotates it right 51 | func (mino *Mino) CloneRotateRight() *Mino { 52 | newMino := *mino 53 | newMino.RotateRight() 54 | return &newMino 55 | } 56 | 57 | // RotateRight rotates the mino right 58 | func (mino *Mino) RotateRight() { 59 | mino.rotation++ 60 | if mino.rotation > 3 { 61 | mino.rotation = 0 62 | } 63 | } 64 | 65 | // CloneRotateLeft creates copy of the mino and rotates it left 66 | func (mino *Mino) CloneRotateLeft() *Mino { 67 | newMino := *mino 68 | newMino.RotateLeft() 69 | return &newMino 70 | } 71 | 72 | // RotateLeft rotates the mino left 73 | func (mino *Mino) RotateLeft() { 74 | if mino.rotation < 1 { 75 | mino.rotation = 3 76 | return 77 | } 78 | mino.rotation-- 79 | } 80 | 81 | // CloneMoveDown creates copy of the mino and moves it down 82 | func (mino *Mino) CloneMoveDown() *Mino { 83 | newMino := *mino 84 | newMino.MoveDown() 85 | return &newMino 86 | } 87 | 88 | // MoveDown moves the mino down 89 | func (mino *Mino) MoveDown() { 90 | mino.y++ 91 | } 92 | 93 | // MoveUp moves the mino up 94 | func (mino *Mino) MoveUp() { 95 | mino.y-- 96 | } 97 | 98 | // ValidLocation check if the mino is in a valid location 99 | func (mino *Mino) ValidLocation(mustBeOnBoard bool) bool { 100 | minoBlocks := mino.minoRotation[mino.rotation] 101 | for i := 0; i < mino.length; i++ { 102 | for j := 0; j < mino.length; j++ { 103 | if minoBlocks[i][j] == colorBlank { 104 | continue 105 | } 106 | if !board.ValidBlockLocation(mino.x+i, mino.y+j, mustBeOnBoard) { 107 | return false 108 | } 109 | } 110 | } 111 | return true 112 | } 113 | 114 | // SetOnBoard attaches mino to the board 115 | func (mino *Mino) SetOnBoard() { 116 | minoBlocks := mino.minoRotation[mino.rotation] 117 | for i := 0; i < mino.length; i++ { 118 | for j := 0; j < mino.length; j++ { 119 | if minoBlocks[i][j] != colorBlank { 120 | board.SetColor(mino.x+i, mino.y+j, minoBlocks[i][j], mino.rotation) 121 | } 122 | } 123 | } 124 | } 125 | 126 | // DrawMino draws the mino on the board 127 | func (mino *Mino) DrawMino(minoType MinoType) { 128 | minoBlocks := mino.minoRotation[mino.rotation] 129 | for i := 0; i < mino.length; i++ { 130 | for j := 0; j < mino.length; j++ { 131 | if minoBlocks[i][j] != colorBlank { 132 | switch minoType { 133 | case MinoPreview: 134 | view.DrawPreviewMinoBlock(i, j, minoBlocks[i][j], mino.rotation, mino.length) 135 | case MinoDrop: 136 | view.DrawBlock(mino.x+i, mino.y+j, colorBlank, mino.rotation) 137 | case MinoCurrent: 138 | if ValidDisplayLocation(mino.x+i, mino.y+j) { 139 | view.DrawBlock(mino.x+i, mino.y+j, minoBlocks[i][j], mino.rotation) 140 | } 141 | } 142 | } 143 | } 144 | } 145 | } 146 | 147 | // minoOverlap check if a mino overlaps another mino 148 | func (mino *Mino) minoOverlap(mino1 *Mino) bool { 149 | minoBlocks := mino.minoRotation[mino.rotation] 150 | for i := 0; i < mino.length; i++ { 151 | for j := 0; j < mino.length; j++ { 152 | if minoBlocks[i][j] == colorBlank { 153 | continue 154 | } 155 | if mino1.isMinoAtLocation(mino.x+i, mino.y+j) { 156 | return true 157 | } 158 | } 159 | } 160 | return false 161 | } 162 | 163 | // isMinoAtLocation check if a mino block is in a location 164 | func (mino *Mino) isMinoAtLocation(x int, y int) bool { 165 | xIndex := x - mino.x 166 | yIndex := y - mino.y 167 | if xIndex < 0 || xIndex >= mino.length || yIndex < 0 || yIndex >= mino.length { 168 | return false 169 | } 170 | 171 | if mino.minoRotation[mino.rotation][xIndex][yIndex] != colorBlank { 172 | return true 173 | } 174 | 175 | return false 176 | } 177 | 178 | // getMinoColorAtLocation gets the mino color at a location 179 | func (mino *Mino) getMinoColorAtLocation(x int, y int) tcell.Color { 180 | xIndex := x - mino.x 181 | yIndex := y - mino.y 182 | if xIndex < 0 || xIndex >= mino.length || yIndex < 0 || yIndex >= mino.length { 183 | return colorBlank 184 | } 185 | 186 | minoBlocks := mino.minoRotation[mino.rotation] 187 | return minoBlocks[xIndex][yIndex] 188 | } 189 | -------------------------------------------------------------------------------- /ai.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | // NewAi creates a new AI 4 | func NewAi() *Ai { 5 | ai := Ai{} 6 | queue := make([]rune, 1) 7 | queue[0] = 'x' 8 | ai.queue = &queue 9 | return &ai 10 | } 11 | 12 | // ProcessQueue checks AI queue and process key moments 13 | func (ai *Ai) ProcessQueue() { 14 | if ai.newQueue != nil { 15 | ai.queue = ai.newQueue 16 | ai.newQueue = nil 17 | ai.index = 0 18 | } 19 | queue := *ai.queue 20 | // wasd + qe keyboard keys 21 | switch queue[ai.index] { 22 | case 'w': 23 | board.MinoDrop() 24 | case 'a': 25 | board.MinoMoveLeft() 26 | case 'd': 27 | board.MinoMoveRight() 28 | case 'q': 29 | board.MinoRotateLeft() 30 | case 'e': 31 | board.MinoRotateRight() 32 | case 'x': 33 | return 34 | } 35 | ai.index++ 36 | view.RefreshScreen() 37 | } 38 | 39 | // GetBestQueue gets the best queue 40 | func (ai *Ai) GetBestQueue() { 41 | bestScore := -9999999 42 | bestQueue := []rune{'x'} 43 | currentMino := board.currentMino 44 | previewMino := board.previewMino 45 | rotations1 := 5 46 | rotations2 := 5 47 | slides := 5 48 | if board.width > 10 { 49 | slides = 3 50 | } 51 | 52 | switch currentMino.minoRotation[0][1][1] { 53 | case colorCyan, colorGreen, colorRed: 54 | rotations1 = 2 55 | case colorYellow: 56 | rotations1 = 1 57 | } 58 | switch previewMino.minoRotation[0][1][1] { 59 | case colorCyan, colorGreen, colorRed: 60 | rotations2 = 2 61 | case colorYellow: 62 | rotations2 = 1 63 | } 64 | 65 | for slide1 := 0; slide1 < slides; slide1++ { 66 | for move1 := board.width; move1 >= 0; move1-- { 67 | for rotate1 := 0; rotate1 < rotations1; rotate1++ { 68 | 69 | queue, mino1 := board.getMovesforMino(rotate1, move1, slide1, currentMino, nil) 70 | if mino1 == nil { 71 | continue 72 | } 73 | 74 | for slide2 := 0; slide2 < slides; slide2++ { 75 | for move2 := board.width; move2 >= 0; move2-- { 76 | for rotate2 := 0; rotate2 < rotations2; rotate2++ { 77 | 78 | _, mino2 := board.getMovesforMino(rotate2, move2, slide2, previewMino, mino1) 79 | if mino2 == nil { 80 | continue 81 | } 82 | 83 | fullLines, holes, bumpy, heightEnds := board.boardStatsWithMinos(mino1, mino2) 84 | score := ai.getScoreFromBoardStats(fullLines, holes, bumpy, heightEnds, mino1.y, mino2.y) 85 | 86 | if score > bestScore { 87 | bestScore = score 88 | bestQueue = queue 89 | } 90 | 91 | } 92 | } 93 | } 94 | 95 | } 96 | } 97 | } 98 | 99 | ai.newQueue = &bestQueue 100 | } 101 | 102 | func (board *Board) getMovesforMino(rotate int, move int, slide int, mino1 *Mino, mino2 *Mino) ([]rune, *Mino) { 103 | var i int 104 | queue := make([]rune, 0, (rotate/2+1)+(move/2+1)+(slide/2+1)+1) 105 | mino := *mino1 106 | 107 | mino.MoveDown() 108 | 109 | if rotate%2 == 0 { 110 | rotate /= 2 111 | for i = 0; i < rotate; i++ { 112 | mino.RotateRight() 113 | if !mino.ValidLocation(false) || (mino2 != nil && mino2.minoOverlap(&mino)) { 114 | return queue, nil 115 | } 116 | queue = append(queue, 'e') 117 | } 118 | } else { 119 | rotate = rotate/2 + 1 120 | for i = 0; i < rotate; i++ { 121 | mino.RotateLeft() 122 | if !mino.ValidLocation(false) || (mino2 != nil && mino2.minoOverlap(&mino)) { 123 | return queue, nil 124 | } 125 | queue = append(queue, 'q') 126 | } 127 | } 128 | 129 | if move%2 == 0 { 130 | move /= 2 131 | for i = 0; i < move; i++ { 132 | mino.MoveRight() 133 | if !mino.ValidLocation(false) || (mino2 != nil && mino2.minoOverlap(&mino)) { 134 | return queue, nil 135 | } 136 | queue = append(queue, 'd') 137 | } 138 | } else { 139 | move = move/2 + 1 140 | for i = 0; i < move; i++ { 141 | mino.MoveLeft() 142 | if !mino.ValidLocation(false) || (mino2 != nil && mino2.minoOverlap(&mino)) { 143 | return queue, nil 144 | } 145 | queue = append(queue, 'a') 146 | } 147 | } 148 | for mino.ValidLocation(false) && (mino2 == nil || !mino2.minoOverlap(&mino)) { 149 | mino.MoveDown() 150 | } 151 | mino.MoveUp() 152 | queue = append(queue, 'w') 153 | 154 | if slide%2 == 0 { 155 | slide /= 2 156 | for i = 0; i < slide; i++ { 157 | mino.MoveLeft() 158 | if !mino.ValidLocation(false) || (mino2 != nil && mino2.minoOverlap(&mino)) { 159 | return queue, nil 160 | } 161 | queue = append(queue, 'a') 162 | } 163 | } else { 164 | slide = slide/2 + 1 165 | for i = 0; i < slide; i++ { 166 | mino.MoveRight() 167 | if !mino.ValidLocation(false) || (mino2 != nil && mino2.minoOverlap(&mino)) { 168 | return queue, nil 169 | } 170 | queue = append(queue, 'd') 171 | } 172 | } 173 | 174 | if !mino.ValidLocation(true) { 175 | return queue, nil 176 | } 177 | 178 | return append(queue, 'x'), &mino 179 | } 180 | 181 | func (board *Board) boardStatsWithMinos(mino1 *Mino, mino2 *Mino) (fullLines int, holes int, bumpy int, heightEnds int) { 182 | var i int 183 | var j int 184 | 185 | // fullLines 186 | for j = 0; j < board.height; j++ { 187 | board.fullLinesY[j] = true 188 | for i = 0; i < board.width; i++ { 189 | if board.colors[i][j] == colorBlank && !mino1.isMinoAtLocation(i, j) && !mino2.isMinoAtLocation(i, j) { 190 | board.fullLinesY[j] = false 191 | break 192 | } 193 | } 194 | if board.fullLinesY[j] { 195 | fullLines++ 196 | } 197 | } 198 | 199 | // holes and bumpy 200 | var foundLast int 201 | var fullLinesFound int 202 | for i = 0; i < board.width; i++ { 203 | found := board.height 204 | fullLinesFound = 0 205 | for j = 0; j < board.height; j++ { 206 | if board.fullLinesY[j] { 207 | fullLinesFound++ 208 | } else { 209 | if board.colors[i][j] != colorBlank || mino1.isMinoAtLocation(i, j) || mino2.isMinoAtLocation(i, j) { 210 | found = j 211 | break 212 | } 213 | } 214 | } 215 | 216 | if i == 0 { 217 | heightEnds = board.height - (found + fullLines - fullLinesFound) 218 | } else { 219 | diffrence := (found + fullLines - fullLinesFound) - foundLast 220 | if diffrence < 0 { 221 | diffrence = -diffrence 222 | } 223 | bumpy += diffrence 224 | } 225 | foundLast = found + fullLines - fullLinesFound 226 | 227 | for j++; j < board.height; j++ { 228 | if board.colors[i][j] == colorBlank && !mino1.isMinoAtLocation(i, j) && !mino2.isMinoAtLocation(i, j) { 229 | holes++ 230 | } 231 | } 232 | } 233 | 234 | heightEnds += board.height - foundLast 235 | 236 | return 237 | } 238 | 239 | func (ai *Ai) getScoreFromBoardStats(fullLines int, holes int, bumpy int, heightEnds int, height1 int, height2 int) int { 240 | score := 8 * heightEnds 241 | if fullLines > 3 { 242 | score += 512 243 | } 244 | score -= 75 * holes 245 | score -= 15 * bumpy 246 | if height1 < 6 { 247 | score -= 10 * (5 - height1) 248 | } 249 | if height2 < 6 { 250 | score -= 10 * (5 - height2) 251 | } 252 | return score 253 | } 254 | -------------------------------------------------------------------------------- /engine.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/gdamore/tcell" 7 | ) 8 | 9 | // EventEngineStopRun stop the run of the engine 10 | type EventEngineStopRun struct { 11 | EventGame 12 | } 13 | 14 | // NewEngine creates new engine 15 | func NewEngine() { 16 | engine = &Engine{ 17 | chanStop: make(chan struct{}), 18 | chanEventKey: make(chan *tcell.EventKey, 8), 19 | mode: engineModeGameOver, 20 | tickTime: time.Hour, 21 | ranking: NewRanking(), 22 | ai: NewAi(), 23 | } 24 | board.Clear() 25 | go engine.Run() 26 | } 27 | 28 | // Run runs the engine 29 | func (engine *Engine) Run() { 30 | logger.Println("Engine Run start") 31 | 32 | var event tcell.Event 33 | 34 | loop: 35 | for { 36 | event = screen.PollEvent() 37 | switch eventType := event.(type) { 38 | case *tcell.EventKey: 39 | select { 40 | case engine.chanEventKey <- eventType: 41 | default: 42 | } 43 | case *EventEngineStopRun: 44 | break loop 45 | case *tcell.EventResize: 46 | view.RefreshScreen() 47 | default: 48 | logger.Printf("event type %T", eventType) 49 | } 50 | } 51 | 52 | logger.Println("Engine Run end") 53 | } 54 | 55 | // Start the game 56 | func (engine *Engine) Start() { 57 | logger.Println("Engine Start start") 58 | 59 | engine.timer = time.NewTimer(engine.tickTime) 60 | engine.timer.Stop() 61 | engine.aiTimer = time.NewTimer(engine.tickTime) 62 | engine.aiTimer.Stop() 63 | 64 | view.RefreshScreen() 65 | 66 | var eventKey *tcell.EventKey 67 | 68 | loop: 69 | for { 70 | select { 71 | case <-engine.chanStop: 72 | break loop 73 | default: 74 | select { 75 | case eventKey = <-engine.chanEventKey: 76 | engine.ProcessEventKey(eventKey) 77 | view.RefreshScreen() 78 | case <-engine.timer.C: 79 | engine.tick() 80 | case <-engine.aiTimer.C: 81 | engine.ai.ProcessQueue() 82 | engine.aiTimer.Reset(engine.tickTime / aiTickDivider) 83 | case <-engine.chanStop: 84 | break loop 85 | } 86 | } 87 | } 88 | 89 | screen.PostEventWait(&EventEngineStopRun{}) 90 | 91 | logger.Println("Engine Start end") 92 | } 93 | 94 | // Stop the game 95 | func (engine *Engine) Stop() { 96 | logger.Println("Engine Stop start") 97 | 98 | if !engine.stopped { 99 | engine.stopped = true 100 | engine.mode = engineModeStopped 101 | close(engine.chanStop) 102 | } 103 | engine.timer.Stop() 104 | engine.aiTimer.Stop() 105 | 106 | logger.Println("Engine Stop end") 107 | } 108 | 109 | // Pause the game 110 | func (engine *Engine) Pause() { 111 | if !engine.timer.Stop() { 112 | select { 113 | case <-engine.timer.C: 114 | default: 115 | } 116 | } 117 | if !engine.aiTimer.Stop() { 118 | select { 119 | case <-engine.aiTimer.C: 120 | default: 121 | } 122 | } 123 | engine.mode = engineModePaused 124 | } 125 | 126 | // UnPause the game 127 | func (engine *Engine) UnPause() { 128 | engine.timer.Reset(engine.tickTime) 129 | if engine.aiEnabled { 130 | engine.aiTimer.Reset(engine.tickTime / aiTickDivider) 131 | engine.mode = engineModeRunWithAI 132 | } else { 133 | engine.mode = engineModeRun 134 | } 135 | } 136 | 137 | // PreviewBoard sets previewBoard to true 138 | func (engine *Engine) PreviewBoard() { 139 | engine.mode = engineModePreview 140 | } 141 | 142 | // NewGame resets board and starts a new game 143 | func (engine *Engine) NewGame() { 144 | logger.Println("Engine NewGame start") 145 | 146 | board.Clear() 147 | engine.tickTime = 480 * time.Millisecond 148 | engine.score = 0 149 | engine.level = 1 150 | engine.deleteLines = 0 151 | 152 | loop: 153 | for { 154 | select { 155 | case <-engine.chanEventKey: 156 | default: 157 | break loop 158 | } 159 | } 160 | 161 | if engine.aiEnabled { 162 | engine.ai.GetBestQueue() 163 | engine.mode = engineModeRunWithAI 164 | } else { 165 | engine.mode = engineModeRun 166 | } 167 | engine.UnPause() 168 | 169 | logger.Println("Engine NewGame end") 170 | } 171 | 172 | // ResetTimer resets the time for lock delay or tick time 173 | func (engine *Engine) ResetTimer(duration time.Duration) { 174 | if !engine.timer.Stop() { 175 | select { 176 | case <-engine.timer.C: 177 | default: 178 | } 179 | } 180 | if duration == 0 { 181 | // duration 0 means tick time 182 | engine.timer.Reset(engine.tickTime) 183 | } else { 184 | // duration is lock delay 185 | engine.timer.Reset(duration) 186 | } 187 | } 188 | 189 | // AiGetBestQueue calls AI to get best queue 190 | func (engine *Engine) AiGetBestQueue() { 191 | if !engine.aiEnabled { 192 | return 193 | } 194 | go engine.ai.GetBestQueue() 195 | } 196 | 197 | // tick move mino down and refreshes screen 198 | func (engine *Engine) tick() { 199 | board.MinoMoveDown() 200 | view.RefreshScreen() 201 | } 202 | 203 | // AddDeleteLines adds deleted lines to score 204 | func (engine *Engine) AddDeleteLines(lines int) { 205 | engine.deleteLines += lines 206 | if engine.deleteLines > 999999 { 207 | engine.deleteLines = 999999 208 | } 209 | 210 | switch lines { 211 | case 1: 212 | engine.AddScore(40 * (engine.level + 1)) 213 | case 2: 214 | engine.AddScore(100 * (engine.level + 1)) 215 | case 3: 216 | engine.AddScore(300 * (engine.level + 1)) 217 | case 4: 218 | engine.AddScore(1200 * (engine.level + 1)) 219 | } 220 | 221 | if engine.level < engine.deleteLines/10 { 222 | engine.LevelUp() 223 | } 224 | } 225 | 226 | // AddScore adds to score 227 | func (engine *Engine) AddScore(add int) { 228 | engine.score += add 229 | if engine.score > 9999999 { 230 | engine.score = 9999999 231 | } 232 | } 233 | 234 | // LevelUp goes up a level 235 | func (engine *Engine) LevelUp() { 236 | if engine.level >= 30 { 237 | return 238 | } 239 | 240 | engine.level++ 241 | switch { 242 | case engine.level > 29: 243 | engine.tickTime = 10 * time.Millisecond 244 | case engine.level > 25: 245 | engine.tickTime = 20 * time.Millisecond 246 | case engine.level > 19: 247 | // 50 to 30 248 | engine.tickTime = time.Duration(10*(15-engine.level/2)) * time.Millisecond 249 | case engine.level > 9: 250 | // 150 to 60 251 | engine.tickTime = time.Duration(10*(25-engine.level)) * time.Millisecond 252 | default: 253 | // 480 to 160 254 | engine.tickTime = time.Duration(10*(52-4*engine.level)) * time.Millisecond 255 | } 256 | } 257 | 258 | // GameOver pauses engine and sets to game over 259 | func (engine *Engine) GameOver() { 260 | logger.Println("Engine GameOver start") 261 | 262 | engine.Pause() 263 | engine.mode = engineModeGameOver 264 | 265 | view.ShowGameOverAnimation() 266 | 267 | loop: 268 | for { 269 | select { 270 | case <-engine.chanEventKey: 271 | default: 272 | break loop 273 | } 274 | } 275 | 276 | engine.ranking.InsertScore(uint64(engine.score)) 277 | engine.ranking.Save() 278 | 279 | logger.Println("Engine GameOver end") 280 | } 281 | 282 | // EnabledAi enables the AI 283 | func (engine *Engine) EnabledAi() { 284 | engine.aiEnabled = true 285 | go engine.ai.GetBestQueue() 286 | engine.aiTimer.Reset(engine.tickTime / aiTickDivider) 287 | } 288 | 289 | // DisableAi disables the AI 290 | func (engine *Engine) DisableAi() { 291 | engine.aiEnabled = false 292 | engine.mode = engineModeRun 293 | if !engine.aiTimer.Stop() { 294 | select { 295 | case <-engine.aiTimer.C: 296 | default: 297 | } 298 | } 299 | } 300 | 301 | // EnabledEditMode enables edit mode 302 | func (engine *Engine) EnabledEditMode() { 303 | edit.EnabledEditMode() 304 | engine.mode = engineModeEdit 305 | } 306 | 307 | // DisableEditMode disables edit mode 308 | func (engine *Engine) DisableEditMode() { 309 | edit.DisableEditMode() 310 | engine.mode = engineModePreview 311 | } 312 | -------------------------------------------------------------------------------- /mino_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestMinoValidLocation(t *testing.T) { 8 | // this must be set to the blank boards 9 | for _, i := range []int{0, 3} { 10 | board.boardsIndex = i 11 | board.Clear() 12 | 13 | tests := []struct { 14 | info string 15 | mino testMinoStruct 16 | changeLocation bool 17 | mustBeOnBoard bool 18 | validLocation bool 19 | }{ 20 | {info: "start 0 false", mino: testMinoStruct{minoRotation: minos.minoBag[0]}, mustBeOnBoard: false, validLocation: true}, 21 | {info: "start 0 true", mino: testMinoStruct{minoRotation: minos.minoBag[0]}, mustBeOnBoard: true, validLocation: true}, 22 | {info: "start 1 false", mino: testMinoStruct{minoRotation: minos.minoBag[1]}, mustBeOnBoard: false, validLocation: true}, 23 | {info: "start 1 true", mino: testMinoStruct{minoRotation: minos.minoBag[1]}, mustBeOnBoard: true, validLocation: false}, 24 | {info: "start 2 false", mino: testMinoStruct{minoRotation: minos.minoBag[2]}, mustBeOnBoard: false, validLocation: true}, 25 | {info: "start 2 true", mino: testMinoStruct{minoRotation: minos.minoBag[2]}, mustBeOnBoard: true, validLocation: false}, 26 | {info: "start 3 false", mino: testMinoStruct{minoRotation: minos.minoBag[3]}, mustBeOnBoard: false, validLocation: true}, 27 | {info: "start 3 true", mino: testMinoStruct{minoRotation: minos.minoBag[3]}, mustBeOnBoard: true, validLocation: false}, 28 | {info: "start 4 false", mino: testMinoStruct{minoRotation: minos.minoBag[4]}, mustBeOnBoard: false, validLocation: true}, 29 | {info: "start 4 true", mino: testMinoStruct{minoRotation: minos.minoBag[4]}, mustBeOnBoard: true, validLocation: false}, 30 | {info: "start 5 false", mino: testMinoStruct{minoRotation: minos.minoBag[5]}, mustBeOnBoard: false, validLocation: true}, 31 | {info: "start 5 true", mino: testMinoStruct{minoRotation: minos.minoBag[5]}, mustBeOnBoard: true, validLocation: false}, 32 | {info: "start 6 false", mino: testMinoStruct{minoRotation: minos.minoBag[6]}, mustBeOnBoard: false, validLocation: true}, 33 | {info: "start 6 true", mino: testMinoStruct{minoRotation: minos.minoBag[6]}, mustBeOnBoard: true, validLocation: false}, 34 | 35 | {info: "top left 3 false", mino: testMinoStruct{minoRotation: minos.minoBag[3], x: 0, y: 0}, changeLocation: true, mustBeOnBoard: false, validLocation: true}, 36 | {info: "top left 3 true", mino: testMinoStruct{minoRotation: minos.minoBag[3], x: 0, y: 0}, changeLocation: true, mustBeOnBoard: true, validLocation: true}, 37 | {info: "top right 3 false", mino: testMinoStruct{minoRotation: minos.minoBag[3], x: board.width - 2, y: 0}, changeLocation: true, mustBeOnBoard: false, validLocation: true}, 38 | {info: "top right 3 true", mino: testMinoStruct{minoRotation: minos.minoBag[3], x: board.width - 2, y: 0}, changeLocation: true, mustBeOnBoard: true, validLocation: true}, 39 | {info: "bottom right 3 false", mino: testMinoStruct{minoRotation: minos.minoBag[3], x: board.width - 2, y: board.height - 2}, changeLocation: true, mustBeOnBoard: false, validLocation: true}, 40 | {info: "bottom right 3 true", mino: testMinoStruct{minoRotation: minos.minoBag[3], x: board.width - 2, y: board.height - 2}, changeLocation: true, mustBeOnBoard: true, validLocation: true}, 41 | {info: "bottom left 3 false", mino: testMinoStruct{minoRotation: minos.minoBag[3], x: 0, y: board.height - 2}, changeLocation: true, mustBeOnBoard: false, validLocation: true}, 42 | {info: "bottom left 3 true", mino: testMinoStruct{minoRotation: minos.minoBag[3], x: 0, y: board.height - 2}, changeLocation: true, mustBeOnBoard: true, validLocation: true}, 43 | 44 | {info: "up 1 top left 3 false", mino: testMinoStruct{minoRotation: minos.minoBag[3], x: 0, y: -1}, changeLocation: true, mustBeOnBoard: false, validLocation: true}, 45 | {info: "up 1 top left 3 true", mino: testMinoStruct{minoRotation: minos.minoBag[3], x: 0, y: -1}, changeLocation: true, mustBeOnBoard: true, validLocation: false}, 46 | {info: "up 1 top right 3 false", mino: testMinoStruct{minoRotation: minos.minoBag[3], x: board.width - 2, y: -1}, changeLocation: true, mustBeOnBoard: false, validLocation: true}, 47 | {info: "up 1 top right 3 true", mino: testMinoStruct{minoRotation: minos.minoBag[3], x: board.width - 2, y: -1}, changeLocation: true, mustBeOnBoard: true, validLocation: false}, 48 | 49 | {info: "up 2 top left 3 false", mino: testMinoStruct{minoRotation: minos.minoBag[3], x: 0, y: -2}, changeLocation: true, mustBeOnBoard: false, validLocation: true}, 50 | {info: "up 2 top left 3 true", mino: testMinoStruct{minoRotation: minos.minoBag[3], x: 0, y: -2}, changeLocation: true, mustBeOnBoard: true, validLocation: false}, 51 | {info: "up 2 top right 3 false", mino: testMinoStruct{minoRotation: minos.minoBag[3], x: board.width - 2, y: -2}, changeLocation: true, mustBeOnBoard: false, validLocation: true}, 52 | {info: "up 2 top right 3 true", mino: testMinoStruct{minoRotation: minos.minoBag[3], x: board.width - 2, y: -2}, changeLocation: true, mustBeOnBoard: true, validLocation: false}, 53 | 54 | {info: "up 3 top left 3 false", mino: testMinoStruct{minoRotation: minos.minoBag[3], x: 0, y: -3}, changeLocation: true, mustBeOnBoard: false, validLocation: false}, 55 | {info: "up 3 top left 3 true", mino: testMinoStruct{minoRotation: minos.minoBag[3], x: 0, y: -3}, changeLocation: true, mustBeOnBoard: true, validLocation: false}, 56 | {info: "up 3 top right 3 false", mino: testMinoStruct{minoRotation: minos.minoBag[3], x: board.width - 2, y: -3}, changeLocation: true, mustBeOnBoard: false, validLocation: false}, 57 | {info: "up 3 top right 3 true", mino: testMinoStruct{minoRotation: minos.minoBag[3], x: board.width - 2, y: -3}, changeLocation: true, mustBeOnBoard: true, validLocation: false}, 58 | 59 | {info: "off top left 3 false", mino: testMinoStruct{minoRotation: minos.minoBag[3], x: -1, y: 0}, changeLocation: true, mustBeOnBoard: false, validLocation: false}, 60 | {info: "off top left 3 true", mino: testMinoStruct{minoRotation: minos.minoBag[3], x: -1, y: 0}, changeLocation: true, mustBeOnBoard: true, validLocation: false}, 61 | {info: "off top right 3 false", mino: testMinoStruct{minoRotation: minos.minoBag[3], x: board.width - 1, y: 0}, changeLocation: true, mustBeOnBoard: false, validLocation: false}, 62 | {info: "off top right 3 true", mino: testMinoStruct{minoRotation: minos.minoBag[3], x: board.width - 1, y: 0}, changeLocation: true, mustBeOnBoard: true, validLocation: false}, 63 | {info: "off bottom right 3 false", mino: testMinoStruct{minoRotation: minos.minoBag[3], x: board.width - 1, y: board.height - 2}, changeLocation: true, mustBeOnBoard: false, validLocation: false}, 64 | {info: "off bottom right 3 true", mino: testMinoStruct{minoRotation: minos.minoBag[3], x: board.width - 1, y: board.height - 2}, changeLocation: true, mustBeOnBoard: true, validLocation: false}, 65 | {info: "off bottom right 3 false", mino: testMinoStruct{minoRotation: minos.minoBag[3], x: board.width - 2, y: board.height - 1}, changeLocation: true, mustBeOnBoard: false, validLocation: false}, 66 | {info: "off bottom right 3 true", mino: testMinoStruct{minoRotation: minos.minoBag[3], x: board.width - 2, y: board.height - 1}, changeLocation: true, mustBeOnBoard: true, validLocation: false}, 67 | {info: "off bottom left 3 false", mino: testMinoStruct{minoRotation: minos.minoBag[3], x: -1, y: board.height - 2}, changeLocation: true, mustBeOnBoard: false, validLocation: false}, 68 | {info: "off bottom left 3 true", mino: testMinoStruct{minoRotation: minos.minoBag[3], x: -1, y: board.height - 2}, changeLocation: true, mustBeOnBoard: true, validLocation: false}, 69 | {info: "off bottom left 3 false", mino: testMinoStruct{minoRotation: minos.minoBag[3], x: 0, y: board.height - 1}, changeLocation: true, mustBeOnBoard: false, validLocation: false}, 70 | {info: "off bottom left 3 true", mino: testMinoStruct{minoRotation: minos.minoBag[3], x: 0, y: board.height - 1}, changeLocation: true, mustBeOnBoard: true, validLocation: false}, 71 | } 72 | 73 | for _, test := range tests { 74 | mino := NewMino() 75 | mino.minoRotation = test.mino.minoRotation 76 | mino.length = len(mino.minoRotation[0]) 77 | if test.changeLocation { 78 | mino.x = test.mino.x 79 | mino.y = test.mino.y 80 | } 81 | validLocation := mino.ValidLocation(test.mustBeOnBoard) 82 | if validLocation != test.validLocation { 83 | lines := board.getDebugBoardWithMino(mino) 84 | for i := 0; i < len(lines); i++ { 85 | t.Log(lines[i]) 86 | } 87 | t.Errorf("MinoValidLocation validLocation - received: %v - expected: %v - info %v", validLocation, test.validLocation, test.info) 88 | } 89 | } 90 | 91 | } 92 | 93 | } 94 | -------------------------------------------------------------------------------- /board.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | 7 | "github.com/gdamore/tcell" 8 | ) 9 | 10 | // NewBoard creates a new clear board 11 | func NewBoard() { 12 | board = &Board{} 13 | board.Clear() 14 | } 15 | 16 | // ChangeBoardSize changes board size 17 | func ChangeBoardSize(width int, height int) { 18 | if board.width == width && board.height == height { 19 | return 20 | } 21 | 22 | newBoard := &Board{width: width, height: height, boardsIndex: board.boardsIndex} 23 | newBoard.colors = make([][]tcell.Color, width) 24 | for i := 0; i < width; i++ { 25 | newBoard.colors[i] = make([]tcell.Color, height) 26 | for j := 0; j < height; j++ { 27 | if i < board.width && j < board.height { 28 | newBoard.colors[i][j] = board.colors[i][j] 29 | } else { 30 | newBoard.colors[i][j] = colorBlank 31 | } 32 | } 33 | } 34 | newBoard.rotation = make([][]int, width) 35 | for i := 0; i < width; i++ { 36 | newBoard.rotation[i] = make([]int, height) 37 | for j := 0; j < height; j++ { 38 | if i < board.width && j < board.height { 39 | newBoard.rotation[i][j] = board.rotation[i][j] 40 | } else { 41 | break 42 | } 43 | } 44 | } 45 | 46 | board = newBoard 47 | board.fullLinesY = make([]bool, board.height) 48 | board.previewMino = NewMino() 49 | board.currentMino = NewMino() 50 | } 51 | 52 | // Clear clears the board to original state 53 | func (board *Board) Clear() { 54 | board.width = len(boards[board.boardsIndex].colors) 55 | board.height = len(boards[board.boardsIndex].colors[0]) 56 | board.colors = make([][]tcell.Color, board.width) 57 | for i := 0; i < board.width; i++ { 58 | board.colors[i] = make([]tcell.Color, board.height) 59 | copy(board.colors[i], boards[board.boardsIndex].colors[i]) 60 | } 61 | board.rotation = make([][]int, board.width) 62 | for i := 0; i < board.width; i++ { 63 | board.rotation[i] = make([]int, board.height) 64 | copy(board.rotation[i], boards[board.boardsIndex].rotation[i]) 65 | } 66 | board.fullLinesY = make([]bool, board.height) 67 | board.previewMino = NewMino() 68 | board.currentMino = NewMino() 69 | } 70 | 71 | // EmptyBoard removes all blocks/colors from the board 72 | func (board *Board) EmptyBoard() { 73 | for i := 0; i < board.width; i++ { 74 | for j := 0; j < board.height; j++ { 75 | board.colors[i][j] = colorBlank 76 | } 77 | } 78 | for i := 0; i < board.width; i++ { 79 | for j := 0; j < board.height; j++ { 80 | board.rotation[i][j] = 0 81 | } 82 | } 83 | } 84 | 85 | // PreviousBoard switches to previous board 86 | func (board *Board) PreviousBoard() { 87 | board.boardsIndex-- 88 | if board.boardsIndex < 0 { 89 | board.boardsIndex = len(boards) - 1 90 | } 91 | engine.PreviewBoard() 92 | board.Clear() 93 | } 94 | 95 | // NextBoard switches to next board 96 | func (board *Board) NextBoard() { 97 | board.boardsIndex++ 98 | if board.boardsIndex == len(boards) { 99 | board.boardsIndex = 0 100 | } 101 | engine.PreviewBoard() 102 | board.Clear() 103 | } 104 | 105 | // MinoMoveLeft moves mino left 106 | func (board *Board) MinoMoveLeft() { 107 | board.dropDistance = 0 108 | mino := board.currentMino.CloneMoveLeft() 109 | if mino.ValidLocation(false) { 110 | board.currentMino = mino 111 | board.StartLockDelayIfBottom() 112 | } 113 | } 114 | 115 | // MinoMoveRight moves mino right 116 | func (board *Board) MinoMoveRight() { 117 | board.dropDistance = 0 118 | mino := board.currentMino.CloneMoveRight() 119 | if mino.ValidLocation(false) { 120 | board.currentMino = mino 121 | board.StartLockDelayIfBottom() 122 | } 123 | } 124 | 125 | // MinoRotateRight rotates mino right 126 | func (board *Board) MinoRotateRight() { 127 | board.dropDistance = 0 128 | mino := board.currentMino.CloneRotateRight() 129 | if mino.ValidLocation(false) { 130 | board.currentMino = mino 131 | board.StartLockDelayIfBottom() 132 | return 133 | } 134 | mino.MoveLeft() 135 | if mino.ValidLocation(false) { 136 | board.currentMino = mino 137 | board.StartLockDelayIfBottom() 138 | return 139 | } 140 | mino.MoveRight() 141 | mino.MoveRight() 142 | if mino.ValidLocation(false) { 143 | board.currentMino = mino 144 | board.StartLockDelayIfBottom() 145 | return 146 | } 147 | } 148 | 149 | // MinoRotateLeft rotates mino right 150 | func (board *Board) MinoRotateLeft() { 151 | board.dropDistance = 0 152 | mino := board.currentMino.CloneRotateLeft() 153 | if mino.ValidLocation(false) { 154 | board.currentMino = mino 155 | board.StartLockDelayIfBottom() 156 | return 157 | } 158 | mino.MoveLeft() 159 | if mino.ValidLocation(false) { 160 | board.currentMino = mino 161 | board.StartLockDelayIfBottom() 162 | return 163 | } 164 | mino.MoveRight() 165 | mino.MoveRight() 166 | if mino.ValidLocation(false) { 167 | board.currentMino = mino 168 | board.StartLockDelayIfBottom() 169 | return 170 | } 171 | } 172 | 173 | // MinoMoveDown moves mino down 174 | func (board *Board) MinoMoveDown() { 175 | mino := board.currentMino.CloneMoveDown() 176 | if mino.ValidLocation(false) { 177 | board.dropDistance = 0 178 | board.currentMino = mino 179 | if !board.StartLockDelayIfBottom() { 180 | engine.ResetTimer(0) 181 | } 182 | return 183 | } 184 | if !board.currentMino.ValidLocation(true) { 185 | engine.GameOver() 186 | return 187 | } 188 | board.nextMino() 189 | } 190 | 191 | // MinoDrop dropps mino 192 | func (board *Board) MinoDrop() { 193 | board.dropDistance = 0 194 | mino := board.currentMino.CloneMoveDown() 195 | for mino.ValidLocation(false) { 196 | board.dropDistance++ 197 | mino.MoveDown() 198 | } 199 | for i := 0; i < board.dropDistance; i++ { 200 | board.currentMino.MoveDown() 201 | } 202 | if !board.currentMino.ValidLocation(true) { 203 | engine.GameOver() 204 | return 205 | } 206 | if board.dropDistance < 1 { 207 | return 208 | } 209 | if !board.StartLockDelayIfBottom() { 210 | engine.ResetTimer(0) 211 | } 212 | } 213 | 214 | // StartLockDelayIfBottom if at bottom, starts lock delay 215 | func (board *Board) StartLockDelayIfBottom() bool { 216 | mino := board.currentMino.CloneMoveDown() 217 | if mino.ValidLocation(false) { 218 | return false 219 | } 220 | engine.ResetTimer(300 * time.Millisecond) 221 | return true 222 | } 223 | 224 | // nextMino gets next mino 225 | func (board *Board) nextMino() { 226 | engine.AddScore(board.dropDistance) 227 | 228 | board.currentMino.SetOnBoard() 229 | 230 | board.deleteCheck() 231 | 232 | if !board.previewMino.ValidLocation(false) { 233 | board.previewMino.MoveUp() 234 | if !board.previewMino.ValidLocation(false) { 235 | engine.GameOver() 236 | return 237 | } 238 | } 239 | 240 | board.currentMino = board.previewMino 241 | board.previewMino = NewMino() 242 | engine.AiGetBestQueue() 243 | engine.ResetTimer(0) 244 | } 245 | 246 | // deleteCheck checks if there are any lines on the board that can be deleted 247 | func (board *Board) deleteCheck() { 248 | lines := board.fullLines() 249 | if len(lines) < 1 { 250 | return 251 | } 252 | 253 | view.ShowDeleteAnimation(lines) 254 | for _, line := range lines { 255 | board.deleteLine(line) 256 | } 257 | 258 | engine.AddDeleteLines(len(lines)) 259 | } 260 | 261 | // fullLines returns the line numbers that have full lines 262 | func (board *Board) fullLines() []int { 263 | fullLines := make([]int, 0, 1) 264 | for j := 0; j < board.height; j++ { 265 | if board.isFullLine(j) { 266 | fullLines = append(fullLines, j) 267 | } 268 | } 269 | return fullLines 270 | } 271 | 272 | // isFullLine checks if line is full 273 | func (board *Board) isFullLine(j int) bool { 274 | for i := 0; i < board.width; i++ { 275 | if board.colors[i][j] == colorBlank { 276 | return false 277 | } 278 | } 279 | return true 280 | } 281 | 282 | // deleteLine deletes the line 283 | func (board *Board) deleteLine(line int) { 284 | for i := 0; i < board.width; i++ { 285 | board.colors[i][line] = colorBlank 286 | } 287 | for j := line; j > 0; j-- { 288 | for i := 0; i < board.width; i++ { 289 | board.colors[i][j] = board.colors[i][j-1] 290 | board.rotation[i][j] = board.rotation[i][j-1] 291 | } 292 | } 293 | for i := 0; i < board.width; i++ { 294 | board.colors[i][0] = colorBlank 295 | } 296 | } 297 | 298 | // SetColor sets the color and rotation of board location 299 | func (board *Board) SetColor(x int, y int, color tcell.Color, rotation int) { 300 | board.colors[x][y] = color 301 | if rotation < 0 { 302 | return 303 | } 304 | board.rotation[x][y] = rotation 305 | } 306 | 307 | // RotateLeft rotates cell left 308 | func (board *Board) RotateLeft(x int, y int) { 309 | if board.rotation[x][y] == 0 { 310 | board.rotation[x][y] = 3 311 | return 312 | } 313 | board.rotation[x][y]-- 314 | } 315 | 316 | // RotateRight rotates cell right 317 | func (board *Board) RotateRight(x int, y int) { 318 | if board.rotation[x][y] == 3 { 319 | board.rotation[x][y] = 0 320 | return 321 | } 322 | board.rotation[x][y]++ 323 | } 324 | 325 | // ValidBlockLocation checks if block location is vaild 326 | func (board *Board) ValidBlockLocation(x int, y int, mustBeOnBoard bool) bool { 327 | if x < 0 || x >= board.width || y >= board.height { 328 | return false 329 | } 330 | if mustBeOnBoard { 331 | if y < 0 { 332 | return false 333 | } 334 | } else { 335 | if y < -2 { 336 | return false 337 | } 338 | } 339 | if y > -1 { 340 | if board.colors[x][y] != colorBlank { 341 | return false 342 | } 343 | } 344 | return true 345 | } 346 | 347 | // ValidDisplayLocation checks if vaild display location 348 | func ValidDisplayLocation(x int, y int) bool { 349 | return x >= 0 && x < board.width && y >= 0 && y < board.height 350 | } 351 | 352 | // DrawBoard draws the board with help from view 353 | func (board *Board) DrawBoard() { 354 | for i := 0; i < board.width; i++ { 355 | for j := 0; j < board.height; j++ { 356 | if board.colors[i][j] != colorBlank { 357 | view.DrawBlock(i, j, board.colors[i][j], board.rotation[i][j]) 358 | } 359 | } 360 | } 361 | } 362 | 363 | // DrawPreviewMino draws the preview mino 364 | func (board *Board) DrawPreviewMino() { 365 | board.previewMino.DrawMino(MinoPreview) 366 | } 367 | 368 | // DrawCurrentMino draws the current mino 369 | func (board *Board) DrawCurrentMino() { 370 | board.currentMino.DrawMino(MinoCurrent) 371 | } 372 | 373 | // DrawDropMino draws the drop mino 374 | func (board *Board) DrawDropMino() { 375 | mino := board.currentMino.CloneMoveDown() 376 | if !mino.ValidLocation(false) { 377 | return 378 | } 379 | for mino.ValidLocation(false) { 380 | mino.MoveDown() 381 | } 382 | mino.MoveUp() 383 | mino.DrawMino(MinoDrop) 384 | } 385 | 386 | // DrawCursor draws the edit cursor 387 | func (board *Board) DrawCursor(x int, y int) { 388 | view.DrawCursor(x, y, board.colors[x][y]) 389 | } 390 | 391 | // printDebugBoard is for printing board in text for debuging 392 | func (board *Board) printDebugBoard() { 393 | for j := 0; j < board.height; j++ { 394 | for i := 0; i < board.width; i++ { 395 | switch board.colors[i][j] { 396 | case colorBlank: 397 | fmt.Print(".") 398 | case colorBlue: 399 | fmt.Print("B") 400 | case colorCyan: 401 | fmt.Print("C") 402 | case colorGreen: 403 | fmt.Print("G") 404 | case colorMagenta: 405 | fmt.Print("M") 406 | case colorRed: 407 | fmt.Print("R") 408 | case colorWhite: 409 | fmt.Print("W") 410 | case colorYellow: 411 | fmt.Print("Y") 412 | default: 413 | fmt.Print("U") 414 | } 415 | } 416 | fmt.Println("") 417 | } 418 | } 419 | 420 | // getDebugBoard returns board as string for debuging and testing 421 | func (board *Board) getDebugBoard() []string { 422 | lines := make([]string, board.height) 423 | for j := 0; j < board.height; j++ { 424 | for i := 0; i < board.width; i++ { 425 | switch board.colors[i][j] { 426 | case colorBlank: 427 | lines[j] += "." 428 | case colorBlue: 429 | lines[j] += "B" 430 | case colorCyan: 431 | lines[j] += "C" 432 | case colorGreen: 433 | lines[j] += "G" 434 | case colorMagenta: 435 | lines[j] += "M" 436 | case colorRed: 437 | lines[j] += "R" 438 | case colorWhite: 439 | lines[j] += "W" 440 | case colorYellow: 441 | lines[j] += "Y" 442 | default: 443 | lines[j] += "U" 444 | } 445 | } 446 | } 447 | return lines 448 | } 449 | 450 | // getDebugBoardWithMino returns board with mino placed on it 451 | func (board *Board) getDebugBoardWithMino(mino *Mino) []string { 452 | lines := make([]string, board.height) 453 | for j := 0; j < board.height; j++ { 454 | for i := 0; i < board.width; i++ { 455 | switch mino.getMinoColorAtLocation(i, j) { 456 | case colorBlank: 457 | switch board.colors[i][j] { 458 | case colorBlank: 459 | lines[j] += "." 460 | case colorBlue: 461 | lines[j] += "B" 462 | case colorCyan: 463 | lines[j] += "C" 464 | case colorGreen: 465 | lines[j] += "G" 466 | case colorMagenta: 467 | lines[j] += "M" 468 | case colorRed: 469 | lines[j] += "R" 470 | case colorWhite: 471 | lines[j] += "W" 472 | case colorYellow: 473 | lines[j] += "Y" 474 | default: 475 | lines[j] += "U" 476 | } 477 | case colorBlue: 478 | lines[j] += "b" 479 | case colorCyan: 480 | lines[j] += "c" 481 | case colorGreen: 482 | lines[j] += "g" 483 | case colorMagenta: 484 | lines[j] += "m" 485 | case colorRed: 486 | lines[j] += "r" 487 | case colorWhite: 488 | lines[j] += "w" 489 | case colorYellow: 490 | lines[j] += "y" 491 | default: 492 | lines[j] += "u" 493 | } 494 | } 495 | } 496 | return lines 497 | } 498 | -------------------------------------------------------------------------------- /view.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "math/rand" 6 | "time" 7 | 8 | "github.com/gdamore/tcell" 9 | ) 10 | 11 | // NewView creates a new view 12 | func NewView() { 13 | var err error 14 | 15 | screen, err = tcell.NewScreen() 16 | if err != nil { 17 | logger.Fatal("NewScreen error:", err) 18 | } 19 | err = screen.Init() 20 | if err != nil { 21 | logger.Fatal("screen Init error:", err) 22 | } 23 | 24 | screen.Clear() 25 | 26 | view = &View{} 27 | } 28 | 29 | // Stop stops the view 30 | func (view *View) Stop() { 31 | logger.Println("View Stop start") 32 | 33 | screen.Fini() 34 | 35 | logger.Println("View Stop end") 36 | } 37 | 38 | // RefreshScreen refreshes the updated view to the screen 39 | func (view *View) RefreshScreen() { 40 | 41 | switch engine.mode { 42 | 43 | case engineModeRun, engineModeRunWithAI: 44 | view.drawBoardBoarder() 45 | view.drawPreviewBoarder() 46 | view.drawTexts() 47 | board.DrawBoard() 48 | board.DrawPreviewMino() 49 | board.DrawDropMino() 50 | board.DrawCurrentMino() 51 | screen.Show() 52 | 53 | case engineModePaused: 54 | screen.Fill(' ', tcell.StyleDefault.Foreground(tcell.ColorBlack).Background(tcell.ColorBlack)) 55 | view.drawBoardBoarder() 56 | view.drawPreviewBoarder() 57 | view.drawTexts() 58 | view.drawPaused() 59 | screen.Show() 60 | 61 | case engineModeGameOver: 62 | view.drawBoardBoarder() 63 | view.drawPreviewBoarder() 64 | view.drawTexts() 65 | view.drawGameOver() 66 | view.drawRankingScores() 67 | screen.Show() 68 | 69 | case engineModePreview: 70 | screen.Fill(' ', tcell.StyleDefault.Foreground(tcell.ColorBlack).Background(tcell.ColorBlack)) 71 | view.drawBoardBoarder() 72 | view.drawPreviewBoarder() 73 | view.drawTexts() 74 | board.DrawBoard() 75 | view.drawGameOver() 76 | screen.Show() 77 | 78 | case engineModeEdit: 79 | screen.Fill(' ', tcell.StyleDefault.Foreground(tcell.ColorBlack).Background(tcell.ColorBlack)) 80 | view.drawBoardBoarder() 81 | board.DrawBoard() 82 | if edit.boardSize { 83 | view.drawEditTextsBoardSize() 84 | } else { 85 | edit.DrawCursor() 86 | view.drawEditTexts() 87 | } 88 | screen.Show() 89 | } 90 | 91 | } 92 | 93 | // drawBoardBoarder draws the board boarder 94 | func (view *View) drawBoardBoarder() { 95 | xOffset := boardXOffset 96 | yOffset := boardYOffset 97 | xEnd := boardXOffset + board.width*2 + 4 98 | yEnd := boardYOffset + board.height + 2 99 | styleBoarder := tcell.StyleDefault.Foreground(tcell.ColorBlack).Background(tcell.ColorLightGray) 100 | styleBoard := tcell.StyleDefault.Foreground(tcell.ColorLightGray).Background(tcell.ColorBlack) 101 | for x := xOffset; x < xEnd; x++ { 102 | for y := yOffset; y < yEnd; y++ { 103 | if x == xOffset || x == xOffset+1 || x == xEnd-1 || x == xEnd-2 || y == yOffset || y == yEnd-1 { 104 | screen.SetContent(x, y, ' ', nil, styleBoarder) 105 | } else { 106 | screen.SetContent(x, y, ' ', nil, styleBoard) 107 | } 108 | } 109 | } 110 | } 111 | 112 | // drawPreviewBoarder draws the preview boarder 113 | func (view *View) drawPreviewBoarder() { 114 | xOffset := boardXOffset + board.width*2 + 8 115 | yOffset := boardYOffset 116 | xEnd := xOffset + 14 117 | yEnd := yOffset + 6 118 | styleBoarder := tcell.StyleDefault.Foreground(tcell.ColorBlack).Background(tcell.ColorLightGray) 119 | styleBoard := tcell.StyleDefault.Foreground(tcell.ColorLightGray).Background(tcell.ColorBlack) 120 | for x := xOffset; x < xEnd; x++ { 121 | for y := yOffset; y < yEnd; y++ { 122 | if x == xOffset || x == xOffset+1 || x == xEnd-1 || x == xEnd-2 || y == yOffset || y == yEnd-1 { 123 | screen.SetContent(x, y, ' ', nil, styleBoarder) 124 | } else { 125 | screen.SetContent(x, y, ' ', nil, styleBoard) 126 | } 127 | } 128 | } 129 | 130 | } 131 | 132 | // drawTexts draws the text 133 | func (view *View) drawTexts() { 134 | xOffset := boardXOffset + board.width*2 + 8 135 | yOffset := boardYOffset + 7 136 | 137 | view.drawText(xOffset, yOffset, "SCORE:", tcell.ColorLightGray, tcell.ColorDarkBlue) 138 | view.drawText(xOffset+7, yOffset, fmt.Sprintf("%7d", engine.score), tcell.ColorBlack, tcell.ColorLightGray) 139 | 140 | yOffset += 2 141 | 142 | view.drawText(xOffset, yOffset, "LINES:", tcell.ColorLightGray, tcell.ColorDarkBlue) 143 | view.drawText(xOffset+7, yOffset, fmt.Sprintf("%7d", engine.deleteLines), tcell.ColorBlack, tcell.ColorLightGray) 144 | 145 | yOffset += 2 146 | 147 | view.drawText(xOffset, yOffset, "LEVEL:", tcell.ColorLightGray, tcell.ColorDarkBlue) 148 | view.drawText(xOffset+7, yOffset, fmt.Sprintf("%4d", engine.level), tcell.ColorBlack, tcell.ColorLightGray) 149 | 150 | yOffset += 2 151 | 152 | // ascii arrow characters add extra two spaces 153 | view.drawText(xOffset, yOffset, "← - left", tcell.ColorLightGray, tcell.ColorBlack) 154 | yOffset++ 155 | view.drawText(xOffset, yOffset, "→ - right", tcell.ColorLightGray, tcell.ColorBlack) 156 | yOffset++ 157 | view.drawText(xOffset, yOffset, "↓ - soft drop", tcell.ColorLightGray, tcell.ColorBlack) 158 | yOffset++ 159 | view.drawText(xOffset, yOffset, "↑ - hard drop", tcell.ColorLightGray, tcell.ColorBlack) 160 | yOffset++ 161 | view.drawText(xOffset, yOffset, "sbar - hard drop", tcell.ColorLightGray, tcell.ColorBlack) 162 | yOffset++ 163 | view.drawText(xOffset, yOffset, "z - rotate left", tcell.ColorLightGray, tcell.ColorBlack) 164 | yOffset++ 165 | view.drawText(xOffset, yOffset, "x - rotate right", tcell.ColorLightGray, tcell.ColorBlack) 166 | yOffset++ 167 | view.drawText(xOffset, yOffset, "p - pause", tcell.ColorLightGray, tcell.ColorBlack) 168 | yOffset++ 169 | view.drawText(xOffset, yOffset, "q - quit", tcell.ColorLightGray, tcell.ColorBlack) 170 | } 171 | 172 | // drawEditTexts draws the edit text 173 | func (view *View) drawEditTexts() { 174 | xOffset := boardXOffset + board.width*2 + 8 175 | yOffset := boardYOffset 176 | 177 | view.drawText(xOffset, yOffset, "Name:", tcell.ColorLightGray, tcell.ColorDarkBlue) 178 | view.drawText(xOffset+7, yOffset, boards[board.boardsIndex].name, tcell.ColorBlack, tcell.ColorLightGray) 179 | yOffset++ 180 | view.drawText(xOffset, yOffset, "Saved:", tcell.ColorLightGray, tcell.ColorDarkBlue) 181 | if edit.saved { 182 | view.drawText(xOffset+7, yOffset, "yes", tcell.ColorBlack, tcell.ColorLightGray) 183 | } else { 184 | view.drawText(xOffset+7, yOffset, "no", tcell.ColorBlack, tcell.ColorLightGray) 185 | } 186 | 187 | yOffset += 2 188 | 189 | // ascii arrow characters add extra two spaces 190 | view.drawText(xOffset, yOffset, "← - left", tcell.ColorLightGray, tcell.ColorBlack) 191 | yOffset++ 192 | view.drawText(xOffset, yOffset, "→ - right", tcell.ColorLightGray, tcell.ColorBlack) 193 | yOffset++ 194 | view.drawText(xOffset, yOffset, "↓ - down", tcell.ColorLightGray, tcell.ColorBlack) 195 | yOffset++ 196 | view.drawText(xOffset, yOffset, "↑ - up", tcell.ColorLightGray, tcell.ColorBlack) 197 | yOffset++ 198 | view.drawText(xOffset, yOffset, "z - rotate left", tcell.ColorLightGray, tcell.ColorBlack) 199 | yOffset++ 200 | view.drawText(xOffset, yOffset, "x - rotate right", tcell.ColorLightGray, tcell.ColorBlack) 201 | yOffset++ 202 | view.drawText(xOffset, yOffset, "c - cyan", tcell.ColorLightGray, tcell.ColorBlack) 203 | yOffset++ 204 | view.drawText(xOffset, yOffset, "b - blue", tcell.ColorLightGray, tcell.ColorBlack) 205 | yOffset++ 206 | view.drawText(xOffset, yOffset, "w - white", tcell.ColorLightGray, tcell.ColorBlack) 207 | yOffset++ 208 | view.drawText(xOffset, yOffset, "e - yellow", tcell.ColorLightGray, tcell.ColorBlack) 209 | yOffset++ 210 | view.drawText(xOffset, yOffset, "g - green", tcell.ColorLightGray, tcell.ColorBlack) 211 | yOffset++ 212 | view.drawText(xOffset, yOffset, "a - magenta", tcell.ColorLightGray, tcell.ColorBlack) 213 | yOffset++ 214 | view.drawText(xOffset, yOffset, "r - red", tcell.ColorLightGray, tcell.ColorBlack) 215 | yOffset++ 216 | view.drawText(xOffset, yOffset, "f - free", tcell.ColorLightGray, tcell.ColorBlack) 217 | yOffset++ 218 | view.drawText(xOffset, yOffset, "ctrl b - change board size", tcell.ColorLightGray, tcell.ColorBlack) 219 | yOffset++ 220 | view.drawText(xOffset, yOffset, "ctrl s - save board", tcell.ColorLightGray, tcell.ColorBlack) 221 | yOffset++ 222 | view.drawText(xOffset, yOffset, "ctrl n - save board as new", tcell.ColorLightGray, tcell.ColorBlack) 223 | yOffset++ 224 | view.drawText(xOffset, yOffset, "ctrl k - delete board", tcell.ColorLightGray, tcell.ColorBlack) 225 | yOffset++ 226 | view.drawText(xOffset, yOffset, "ctrl o - empty board", tcell.ColorLightGray, tcell.ColorBlack) 227 | yOffset++ 228 | view.drawText(xOffset, yOffset, "ctrl q - quit", tcell.ColorLightGray, tcell.ColorBlack) 229 | } 230 | 231 | // drawEditTextsBoardSize draws the edit text for board size mode 232 | func (view *View) drawEditTextsBoardSize() { 233 | xOffset := boardXOffset + board.width*2 + 8 234 | yOffset := boardYOffset 235 | 236 | view.drawText(xOffset, yOffset, "Name:", tcell.ColorLightGray, tcell.ColorDarkBlue) 237 | view.drawText(xOffset+7, yOffset, boards[board.boardsIndex].name, tcell.ColorBlack, tcell.ColorLightGray) 238 | 239 | yOffset += 2 240 | 241 | view.drawText(xOffset, yOffset, "Size:", tcell.ColorLightGray, tcell.ColorDarkBlue) 242 | view.drawText(xOffset+7, yOffset, fmt.Sprintf("%2d X %2d", edit.width, edit.height), tcell.ColorBlack, tcell.ColorLightGray) 243 | 244 | yOffset += 2 245 | 246 | // ascii arrow characters add extra two spaces 247 | view.drawText(xOffset, yOffset, "← - board width decrement", tcell.ColorLightGray, tcell.ColorBlack) 248 | yOffset++ 249 | view.drawText(xOffset, yOffset, "→ - board width increment", tcell.ColorLightGray, tcell.ColorBlack) 250 | yOffset++ 251 | view.drawText(xOffset, yOffset, "↓ - board height decrement", tcell.ColorLightGray, tcell.ColorBlack) 252 | yOffset++ 253 | view.drawText(xOffset, yOffset, "↑ - board height increment", tcell.ColorLightGray, tcell.ColorBlack) 254 | yOffset++ 255 | view.drawText(xOffset, yOffset, "q - done", tcell.ColorLightGray, tcell.ColorBlack) 256 | } 257 | 258 | // DrawPreviewMinoBlock draws the preview mino 259 | func (view *View) DrawPreviewMinoBlock(x int, y int, color tcell.Color, rotation int, length int) { 260 | char1 := '█' 261 | char2 := '█' 262 | switch rotation { 263 | case 0: 264 | char1 = '▄' 265 | char2 = '▄' 266 | case 1: 267 | char2 = ' ' 268 | case 2: 269 | char1 = '▀' 270 | char2 = '▀' 271 | case 3: 272 | char1 = ' ' 273 | } 274 | xOffset := 2*x + 2*board.width + boardXOffset + 11 + (4 - length) 275 | style := tcell.StyleDefault.Foreground(color).Background(color).Dim(true) 276 | screen.SetContent(xOffset, y+boardYOffset+2, char1, nil, style) 277 | screen.SetContent(xOffset+1, y+boardYOffset+2, char2, nil, style) 278 | } 279 | 280 | // DrawBlock draws a block 281 | func (view *View) DrawBlock(x int, y int, color tcell.Color, rotation int) { 282 | char1 := '█' 283 | char2 := '█' 284 | switch rotation { 285 | case 0: 286 | char1 = '▄' 287 | char2 = '▄' 288 | case 1: 289 | char2 = ' ' 290 | case 2: 291 | char1 = '▀' 292 | char2 = '▀' 293 | case 3: 294 | char1 = ' ' 295 | } 296 | if color == colorBlank { 297 | // colorBlank means drop Mino 298 | style := tcell.StyleDefault.Foreground(tcell.ColorBlack).Background(tcell.ColorSilver).Bold(true) 299 | screen.SetContent(2*x+boardXOffset+2, y+boardYOffset+1, char1, nil, style) 300 | screen.SetContent(2*x+boardXOffset+3, y+boardYOffset+1, char2, nil, style) 301 | } else { 302 | style := tcell.StyleDefault.Foreground(color).Background(color).Dim(true) 303 | screen.SetContent(2*x+boardXOffset+2, y+boardYOffset+1, char1, nil, style) 304 | screen.SetContent(2*x+boardXOffset+3, y+boardYOffset+1, char2, nil, style) 305 | } 306 | } 307 | 308 | // drawPaused draws Paused 309 | func (view *View) drawPaused() { 310 | yOffset := (board.height+1)/2 + boardYOffset 311 | view.drawTextCenter(yOffset, "Paused", tcell.ColorWhite, tcell.ColorBlack) 312 | } 313 | 314 | // drawGameOver draws GAME OVER 315 | func (view *View) drawGameOver() { 316 | yOffset := boardYOffset + 2 317 | view.drawTextCenter(yOffset, " GAME OVER", tcell.ColorWhite, tcell.ColorBlack) 318 | yOffset += 2 319 | view.drawTextCenter(yOffset, "sbar for new game", tcell.ColorWhite, tcell.ColorBlack) 320 | 321 | if engine.mode == engineModePreview { 322 | return 323 | } 324 | 325 | yOffset += 2 326 | // ascii arrow characters add extra two spaces 327 | view.drawTextCenter(yOffset, "←previous board", tcell.ColorWhite, tcell.ColorBlack) 328 | yOffset += 2 329 | view.drawTextCenter(yOffset, "→next board", tcell.ColorWhite, tcell.ColorBlack) 330 | } 331 | 332 | // drawRankingScores draws the ranking scores 333 | func (view *View) drawRankingScores() { 334 | yOffset := boardYOffset + 10 335 | for index, line := range engine.ranking.scores { 336 | view.drawTextCenter(yOffset+index, fmt.Sprintf("%1d: %6d", index+1, line), tcell.ColorWhite, tcell.ColorBlack) 337 | } 338 | } 339 | 340 | // drawText draws the provided text 341 | func (view *View) drawText(x int, y int, text string, fg tcell.Color, bg tcell.Color) { 342 | style := tcell.StyleDefault.Foreground(fg).Background(bg) 343 | for index, char := range text { 344 | screen.SetContent(x+index, y, rune(char), nil, style) 345 | } 346 | } 347 | 348 | // drawTextCenter draws text in the center of the board 349 | func (view *View) drawTextCenter(y int, text string, fg tcell.Color, bg tcell.Color) { 350 | xOffset := board.width - (len(text)+1)/2 + boardXOffset + 2 351 | style := tcell.StyleDefault.Foreground(fg).Background(bg) 352 | for index, char := range text { 353 | screen.SetContent(index+xOffset, y, rune(char), nil, style) 354 | } 355 | } 356 | 357 | // ShowDeleteAnimation draws the delete animation 358 | func (view *View) ShowDeleteAnimation(lines []int) { 359 | view.RefreshScreen() 360 | 361 | for times := 0; times < 3; times++ { 362 | for _, y := range lines { 363 | view.colorizeLine(y, tcell.ColorLightGray) 364 | } 365 | screen.Show() 366 | time.Sleep(140 * time.Millisecond) 367 | 368 | view.RefreshScreen() 369 | time.Sleep(140 * time.Millisecond) 370 | } 371 | } 372 | 373 | // ShowGameOverAnimation draws one randomily picked gave over animation 374 | func (view *View) ShowGameOverAnimation() { 375 | logger.Println("View ShowGameOverAnimation start") 376 | 377 | switch rand.Intn(3) { 378 | case 0: 379 | logger.Println("View ShowGameOverAnimation case 0") 380 | for y := board.height - 1; y >= 0; y-- { 381 | view.colorizeLine(y, tcell.ColorLightGray) 382 | screen.Show() 383 | time.Sleep(60 * time.Millisecond) 384 | } 385 | 386 | case 1: 387 | logger.Println("View ShowGameOverAnimation case 1") 388 | for y := 0; y < board.height; y++ { 389 | view.colorizeLine(y, tcell.ColorLightGray) 390 | screen.Show() 391 | time.Sleep(60 * time.Millisecond) 392 | } 393 | 394 | case 2: 395 | logger.Println("View ShowGameOverAnimation case 2") 396 | sleepTime := 50 * time.Millisecond 397 | topStartX := boardXOffset + 3 398 | topEndX := board.width*2 + boardXOffset + 1 399 | topY := boardYOffset + 1 400 | rightStartY := boardYOffset + 1 401 | rightEndY := board.height + boardYOffset + 1 402 | rightX := board.width*2 + boardXOffset + 1 403 | bottomStartX := topEndX - 1 404 | bottomEndX := topStartX - 1 405 | bottomY := board.height + boardYOffset 406 | leftStartY := rightEndY - 1 407 | leftEndY := rightStartY - 1 408 | leftX := boardXOffset + 2 409 | style := tcell.StyleDefault.Foreground(tcell.ColorLightGray).Background(tcell.ColorLightGray) 410 | 411 | for topStartX <= topEndX && rightStartY <= rightEndY { 412 | for x := topStartX; x < topEndX; x++ { 413 | screen.SetContent(x, topY, ' ', nil, style) 414 | } 415 | topStartX++ 416 | topEndX-- 417 | topY++ 418 | for y := rightStartY; y < rightEndY; y++ { 419 | screen.SetContent(rightX, y, ' ', nil, style) 420 | } 421 | rightStartY++ 422 | rightEndY-- 423 | rightX-- 424 | for x := bottomStartX; x > bottomEndX; x-- { 425 | screen.SetContent(x, bottomY, ' ', nil, style) 426 | } 427 | bottomStartX-- 428 | bottomEndX++ 429 | bottomY-- 430 | for y := leftStartY; y > leftEndY; y-- { 431 | screen.SetContent(leftX, y, ' ', nil, style) 432 | } 433 | leftStartY-- 434 | leftEndY++ 435 | leftX++ 436 | screen.Show() 437 | time.Sleep(sleepTime) 438 | sleepTime += 4 * time.Millisecond 439 | } 440 | } 441 | 442 | logger.Println("View ShowGameOverAnimation end") 443 | } 444 | 445 | // colorizeLine changes the color of a line 446 | func (view *View) colorizeLine(y int, color tcell.Color) { 447 | style := tcell.StyleDefault.Foreground(tcell.ColorBlack).Background(color) 448 | for x := 0; x < board.width; x++ { 449 | screen.SetContent(x*2+boardXOffset+2, y+boardYOffset+1, ' ', nil, style) 450 | screen.SetContent(x*2+boardXOffset+3, y+boardYOffset+1, ' ', nil, style) 451 | } 452 | } 453 | 454 | // DrawCursor draws current cursor location 455 | func (view *View) DrawCursor(x int, y int, color tcell.Color) { 456 | style := tcell.StyleDefault.Foreground(color).Background(tcell.ColorBlack) 457 | if color == colorBlank { 458 | style = tcell.StyleDefault.Foreground(tcell.ColorBlack).Background(tcell.ColorLightGrey) 459 | } 460 | screen.SetContent(x*2+boardXOffset+2, y+boardYOffset+1, '◄', nil, style) 461 | screen.SetContent(x*2+boardXOffset+3, y+boardYOffset+1, '►', nil, style) 462 | screen.Show() 463 | } 464 | -------------------------------------------------------------------------------- /ai_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | type testAiStruct struct { 8 | info string 9 | minos []testMinoStruct 10 | fullLines int 11 | holes int 12 | bumpy int 13 | heightEnds int 14 | } 15 | 16 | func TestAI(t *testing.T) { 17 | // this must be set to the blank 10x20 board 18 | board.boardsIndex = 0 19 | board.Clear() 20 | 21 | tests := []testAiStruct{ 22 | {info: "fullLines 2x minoI", minos: []testMinoStruct{ 23 | {minoRotation: minos.minoBag[0], x: 0, y: 18}, // minoI 24 | {minoRotation: minos.minoBag[0], x: 4, y: 18}, // minoI 25 | }, fullLines: 0, holes: 0, bumpy: 1, heightEnds: 1}, 26 | {info: "fullLines 2x2 minoI", minos: []testMinoStruct{ 27 | {minoRotation: minos.minoBag[0], x: 0, y: 18}, // minoI 28 | {minoRotation: minos.minoBag[0], x: 4, y: 18}, // minoI 29 | {minoRotation: minos.minoBag[0], x: 0, y: 17}, // minoI 30 | {minoRotation: minos.minoBag[0], x: 4, y: 17}, // minoI 31 | }, fullLines: 0, holes: 0, bumpy: 2, heightEnds: 2}, 32 | {info: "fullLines 2x minoI minoO", minos: []testMinoStruct{ 33 | {minoRotation: minos.minoBag[0], x: 0, y: 18}, // minoI 34 | {minoRotation: minos.minoBag[0], x: 4, y: 18}, // minoI 35 | {minoRotation: minos.minoBag[3], x: 8, y: 18}, // minoO 36 | }, fullLines: 1, holes: 0, bumpy: 1, heightEnds: 1}, 37 | {info: "fullLines 2x2 minoI minoO", minos: []testMinoStruct{ 38 | {minoRotation: minos.minoBag[0], x: 0, y: 18}, // minoI 39 | {minoRotation: minos.minoBag[0], x: 4, y: 18}, // minoI 40 | {minoRotation: minos.minoBag[0], x: 0, y: 17}, // minoI 41 | {minoRotation: minos.minoBag[0], x: 4, y: 17}, // minoI 42 | {minoRotation: minos.minoBag[3], x: 8, y: 18}, // minoO 43 | }, fullLines: 2, holes: 0, bumpy: 0, heightEnds: 0}, 44 | {info: "fullLines 5x minoO", minos: []testMinoStruct{ 45 | {minoRotation: minos.minoBag[3], x: 0, y: 18}, // minoO 46 | {minoRotation: minos.minoBag[3], x: 2, y: 18}, // minoO 47 | {minoRotation: minos.minoBag[3], x: 4, y: 18}, // minoO 48 | {minoRotation: minos.minoBag[3], x: 6, y: 18}, // minoO 49 | {minoRotation: minos.minoBag[3], x: 8, y: 18}, // minoO 50 | }, fullLines: 2, holes: 0, bumpy: 0, heightEnds: 0}, 51 | {info: "fullLines 4x4 minoI 2x minoO", minos: []testMinoStruct{ 52 | {minoRotation: minos.minoBag[0], x: 0, y: 18}, // minoI 53 | {minoRotation: minos.minoBag[0], x: 4, y: 18}, // minoI 54 | {minoRotation: minos.minoBag[0], x: 0, y: 17}, // minoI 55 | {minoRotation: minos.minoBag[0], x: 4, y: 17}, // minoI 56 | {minoRotation: minos.minoBag[0], x: 0, y: 16}, // minoI 57 | {minoRotation: minos.minoBag[0], x: 4, y: 16}, // minoI 58 | {minoRotation: minos.minoBag[0], x: 0, y: 15}, // minoI 59 | {minoRotation: minos.minoBag[0], x: 4, y: 15}, // minoI 60 | {minoRotation: minos.minoBag[3], x: 8, y: 18}, // minoO 61 | {minoRotation: minos.minoBag[3], x: 8, y: 16}, // minoO 62 | }, fullLines: 4, holes: 0, bumpy: 0, heightEnds: 0}, 63 | {info: "holes 2x minoI minoO", minos: []testMinoStruct{ 64 | {minoRotation: minos.minoBag[0], x: 0, y: 18}, // minoI 65 | {minoRotation: minos.minoBag[0], x: 6, y: 18}, // minoI 66 | {minoRotation: minos.minoBag[3], x: 4, y: 17}, // minoO 67 | }, fullLines: 0, holes: 2, bumpy: 4, heightEnds: 2}, 68 | {info: "holes 6x minoO", minos: []testMinoStruct{ 69 | {minoRotation: minos.minoBag[3], x: 0, y: 18}, // minoO 70 | {minoRotation: minos.minoBag[3], x: 4, y: 18}, // minoO 71 | {minoRotation: minos.minoBag[3], x: 8, y: 18}, // minoO 72 | {minoRotation: minos.minoBag[3], x: 2, y: 16}, // minoO 73 | {minoRotation: minos.minoBag[3], x: 6, y: 16}, // minoO 74 | }, fullLines: 0, holes: 8, bumpy: 8, heightEnds: 4}, 75 | {info: "holes 4x minoT 2x minoI", minos: []testMinoStruct{ 76 | {minoRotation: minos.minoBag[5], x: 0, y: 18}, // minoT 77 | {minoRotation: minos.minoBag[5], x: 7, y: 18}, // minoT 78 | {minoRotation: minos.minoBag[5], x: 3, y: 16}, // minoT 79 | {minoRotation: minos.minoBag[5], x: 7, y: 16}, // minoT 80 | {minoRotation: minos.minoBag[0], x: 2, y: 14}, // minoI 81 | {minoRotation: minos.minoBag[0], x: 6, y: 14}, // minoI 82 | }, fullLines: 0, holes: 19, bumpy: 4, heightEnds: 6}, 83 | {info: "holes 3x minoZ", minos: []testMinoStruct{ 84 | {minoRotation: minos.minoBag[6], x: 0, y: 18}, // minoZ 85 | {minoRotation: minos.minoBag[6], x: 3, y: 18}, // minoZ 86 | {minoRotation: minos.minoBag[6], x: 6, y: 18}, // minoZ 87 | }, fullLines: 0, holes: 3, bumpy: 6, heightEnds: 2}, 88 | {info: "holes 4x minoT 2x minoI 2x minoO", minos: []testMinoStruct{ 89 | {minoRotation: minos.minoBag[5], x: 0, y: 18}, // minoT 90 | {minoRotation: minos.minoBag[5], x: 7, y: 18}, // minoT 91 | {minoRotation: minos.minoBag[3], x: 0, y: 16}, // minoO 92 | {minoRotation: minos.minoBag[5], x: 3, y: 16}, // minoT 93 | {minoRotation: minos.minoBag[5], x: 7, y: 16}, // minoT 94 | {minoRotation: minos.minoBag[3], x: 0, y: 14}, // minoO 95 | {minoRotation: minos.minoBag[0], x: 2, y: 14}, // minoI 96 | {minoRotation: minos.minoBag[0], x: 6, y: 14}, // minoI 97 | }, fullLines: 1, holes: 9, bumpy: 16, heightEnds: 8}, 98 | {info: "bumpy 2x minoT - 1", minos: []testMinoStruct{ 99 | {minoRotation: minos.minoBag[5], x: 0, y: 18}, // minoT 100 | {minoRotation: minos.minoBag[5], x: 5, y: 18}, // minoT 101 | }, fullLines: 0, holes: 0, bumpy: 7, heightEnds: 1}, 102 | {info: "bumpy 2x minoT - 2", minos: []testMinoStruct{ 103 | {minoRotation: minos.minoBag[5], x: 1, y: 18}, // minoT 104 | {minoRotation: minos.minoBag[5], x: 6, y: 18}, // minoT 105 | }, fullLines: 0, holes: 0, bumpy: 8, heightEnds: 0}, 106 | {info: "bumpy 2x minoT - 3", minos: []testMinoStruct{ 107 | {minoRotation: minos.minoBag[5], x: 2, y: 18}, // minoT 108 | {minoRotation: minos.minoBag[5], x: 7, y: 18}, // minoT 109 | }, fullLines: 0, holes: 0, bumpy: 7, heightEnds: 1}, 110 | {info: "bumpy 2x minoJ - 1", minos: []testMinoStruct{ 111 | {minoRotation: minos.minoBag[1], x: 0, y: 18}, // minoJ 112 | {minoRotation: minos.minoBag[1], x: 5, y: 18}, // minoJ 113 | }, fullLines: 0, holes: 0, bumpy: 6, heightEnds: 2}, 114 | {info: "bumpy 2x minoJ - 2", minos: []testMinoStruct{ 115 | {minoRotation: minos.minoBag[1], x: 1, y: 18}, // minoJ 116 | {minoRotation: minos.minoBag[1], x: 6, y: 18}, // minoJ 117 | }, fullLines: 0, holes: 0, bumpy: 8, heightEnds: 0}, 118 | {info: "bumpy 2x minoJ - 2", minos: []testMinoStruct{ 119 | {minoRotation: minos.minoBag[1], x: 2, y: 18}, // minoJ 120 | {minoRotation: minos.minoBag[1], x: 7, y: 18}, // minoJ 121 | }, fullLines: 0, holes: 0, bumpy: 7, heightEnds: 1}, 122 | {info: "bumpy 2x minoL - 1", minos: []testMinoStruct{ 123 | {minoRotation: minos.minoBag[2], x: 0, y: 18}, // minoL 124 | {minoRotation: minos.minoBag[2], x: 5, y: 18}, // minoL 125 | }, fullLines: 0, holes: 0, bumpy: 7, heightEnds: 1}, 126 | {info: "bumpy 2x minoL - 2", minos: []testMinoStruct{ 127 | {minoRotation: minos.minoBag[2], x: 1, y: 18}, // minoL 128 | {minoRotation: minos.minoBag[2], x: 6, y: 18}, // minoL 129 | }, fullLines: 0, holes: 0, bumpy: 8, heightEnds: 0}, 130 | {info: "bumpy 2x minoL - 3", minos: []testMinoStruct{ 131 | {minoRotation: minos.minoBag[2], x: 2, y: 18}, // minoL 132 | {minoRotation: minos.minoBag[2], x: 7, y: 18}, // minoL 133 | }, fullLines: 0, holes: 0, bumpy: 6, heightEnds: 2}, 134 | } 135 | 136 | runAiTests(t, tests) 137 | } 138 | 139 | func TestBigBoardAI(t *testing.T) { 140 | // this must be set to the blank 20x20 board 141 | board.boardsIndex = 3 142 | board.Clear() 143 | 144 | tests := []testAiStruct{ 145 | {info: "fullLines 4x minoI", minos: []testMinoStruct{ 146 | {minoRotation: minos.minoBag[0], x: 0, y: 18}, // minoI 147 | {minoRotation: minos.minoBag[0], x: 4, y: 18}, // minoI 148 | {minoRotation: minos.minoBag[0], x: 8, y: 18}, // minoI 149 | {minoRotation: minos.minoBag[0], x: 12, y: 18}, // minoI 150 | }, fullLines: 0, holes: 0, bumpy: 1, heightEnds: 1}, 151 | {info: "fullLines 5x minoI", minos: []testMinoStruct{ 152 | {minoRotation: minos.minoBag[0], x: 0, y: 18}, // minoI 153 | {minoRotation: minos.minoBag[0], x: 4, y: 18}, // minoI 154 | {minoRotation: minos.minoBag[0], x: 8, y: 18}, // minoI 155 | {minoRotation: minos.minoBag[0], x: 12, y: 18}, // minoI 156 | {minoRotation: minos.minoBag[0], x: 16, y: 18}, // minoI 157 | }, fullLines: 1, holes: 0, bumpy: 0, heightEnds: 0}, 158 | {info: "fullLines 5x2 minoI", minos: []testMinoStruct{ 159 | {minoRotation: minos.minoBag[0], x: 0, y: 18}, // minoI 160 | {minoRotation: minos.minoBag[0], x: 4, y: 18}, // minoI 161 | {minoRotation: minos.minoBag[0], x: 8, y: 18}, // minoI 162 | {minoRotation: minos.minoBag[0], x: 12, y: 18}, // minoI 163 | {minoRotation: minos.minoBag[0], x: 16, y: 18}, // minoI 164 | {minoRotation: minos.minoBag[0], x: 0, y: 17}, // minoI 165 | {minoRotation: minos.minoBag[0], x: 4, y: 17}, // minoI 166 | {minoRotation: minos.minoBag[0], x: 8, y: 17}, // minoI 167 | {minoRotation: minos.minoBag[0], x: 12, y: 17}, // minoI 168 | {minoRotation: minos.minoBag[0], x: 16, y: 17}, // minoI 169 | }, fullLines: 2, holes: 0, bumpy: 0, heightEnds: 0}, 170 | {info: "fullLines 9x minoO", minos: []testMinoStruct{ 171 | {minoRotation: minos.minoBag[3], x: 0, y: 18}, // minoO 172 | {minoRotation: minos.minoBag[3], x: 2, y: 18}, // minoO 173 | {minoRotation: minos.minoBag[3], x: 4, y: 18}, // minoO 174 | {minoRotation: minos.minoBag[3], x: 6, y: 18}, // minoO 175 | {minoRotation: minos.minoBag[3], x: 8, y: 18}, // minoO 176 | {minoRotation: minos.minoBag[3], x: 10, y: 18}, // minoO 177 | {minoRotation: minos.minoBag[3], x: 12, y: 18}, // minoO 178 | {minoRotation: minos.minoBag[3], x: 14, y: 18}, // minoO 179 | {minoRotation: minos.minoBag[3], x: 16, y: 18}, // minoO 180 | }, fullLines: 0, holes: 0, bumpy: 2, heightEnds: 2}, 181 | {info: "fullLines 10x minoO", minos: []testMinoStruct{ 182 | {minoRotation: minos.minoBag[3], x: 0, y: 18}, // minoO 183 | {minoRotation: minos.minoBag[3], x: 2, y: 18}, // minoO 184 | {minoRotation: minos.minoBag[3], x: 4, y: 18}, // minoO 185 | {minoRotation: minos.minoBag[3], x: 6, y: 18}, // minoO 186 | {minoRotation: minos.minoBag[3], x: 8, y: 18}, // minoO 187 | {minoRotation: minos.minoBag[3], x: 10, y: 18}, // minoO 188 | {minoRotation: minos.minoBag[3], x: 12, y: 18}, // minoO 189 | {minoRotation: minos.minoBag[3], x: 14, y: 18}, // minoO 190 | {minoRotation: minos.minoBag[3], x: 16, y: 18}, // minoO 191 | {minoRotation: minos.minoBag[3], x: 18, y: 18}, // minoO 192 | }, fullLines: 2, holes: 0, bumpy: 0, heightEnds: 0}, 193 | {info: "holes 3x minoO 3x minoI", minos: []testMinoStruct{ 194 | {minoRotation: minos.minoBag[3], x: 0, y: 18}, // minoO 195 | {minoRotation: minos.minoBag[3], x: 6, y: 18}, // minoO 196 | {minoRotation: minos.minoBag[3], x: 12, y: 18}, // minoO 197 | {minoRotation: minos.minoBag[0], x: 2, y: 16}, // minoI 198 | {minoRotation: minos.minoBag[0], x: 8, y: 16}, // minoI 199 | {minoRotation: minos.minoBag[0], x: 14, y: 16}, // minoI 200 | }, fullLines: 0, holes: 24, bumpy: 8, heightEnds: 2}, 201 | {info: "holes 5x minoZ", minos: []testMinoStruct{ 202 | {minoRotation: minos.minoBag[6], x: 0, y: 18}, // minoZ 203 | {minoRotation: minos.minoBag[6], x: 4, y: 18}, // minoZ 204 | {minoRotation: minos.minoBag[6], x: 8, y: 18}, // minoZ 205 | {minoRotation: minos.minoBag[6], x: 12, y: 18}, // minoZ 206 | {minoRotation: minos.minoBag[6], x: 16, y: 18}, // minoZ 207 | }, fullLines: 0, holes: 5, bumpy: 18, heightEnds: 2}, 208 | {info: "holes 6x minoT 2x minoO 5x minoI", minos: []testMinoStruct{ 209 | {minoRotation: minos.minoBag[5], x: 0, y: 18}, // minoT 210 | {minoRotation: minos.minoBag[5], x: 6, y: 18}, // minoT 211 | {minoRotation: minos.minoBag[5], x: 12, y: 18}, // minoT 212 | {minoRotation: minos.minoBag[3], x: 18, y: 18}, // minoO 213 | {minoRotation: minos.minoBag[5], x: 3, y: 16}, // minoT 214 | {minoRotation: minos.minoBag[5], x: 9, y: 16}, // minoT 215 | {minoRotation: minos.minoBag[5], x: 15, y: 16}, // minoT 216 | {minoRotation: minos.minoBag[3], x: 18, y: 16}, // minoO 217 | {minoRotation: minos.minoBag[0], x: 0, y: 14}, // minoI 218 | {minoRotation: minos.minoBag[0], x: 4, y: 14}, // minoI 219 | {minoRotation: minos.minoBag[0], x: 8, y: 14}, // minoI 220 | {minoRotation: minos.minoBag[0], x: 12, y: 14}, // minoI 221 | {minoRotation: minos.minoBag[0], x: 16, y: 14}, // minoI 222 | }, fullLines: 1, holes: 18, bumpy: 23, heightEnds: 5}, 223 | {info: "bumpy 4x minoJ - 1", minos: []testMinoStruct{ 224 | {minoRotation: minos.minoBag[1], x: 0, y: 18}, // minoJ 225 | {minoRotation: minos.minoBag[1], x: 5, y: 18}, // minoJ 226 | {minoRotation: minos.minoBag[1], x: 10, y: 18}, // minoJ 227 | {minoRotation: minos.minoBag[1], x: 15, y: 18}, // minoJ 228 | }, fullLines: 0, holes: 0, bumpy: 14, heightEnds: 2}, 229 | {info: "bumpy 4x minoJ - 2", minos: []testMinoStruct{ 230 | {minoRotation: minos.minoBag[1], x: 1, y: 18}, // minoJ 231 | {minoRotation: minos.minoBag[1], x: 6, y: 18}, // minoJ 232 | {minoRotation: minos.minoBag[1], x: 11, y: 18}, // minoJ 233 | {minoRotation: minos.minoBag[1], x: 16, y: 18}, // minoJ 234 | }, fullLines: 0, holes: 0, bumpy: 16, heightEnds: 0}, 235 | {info: "bumpy 4x minoJ - 3", minos: []testMinoStruct{ 236 | {minoRotation: minos.minoBag[1], x: 2, y: 18}, // minoJ 237 | {minoRotation: minos.minoBag[1], x: 7, y: 18}, // minoJ 238 | {minoRotation: minos.minoBag[1], x: 12, y: 18}, // minoJ 239 | {minoRotation: minos.minoBag[1], x: 17, y: 18}, // minoJ 240 | }, fullLines: 0, holes: 0, bumpy: 15, heightEnds: 1}, 241 | {info: "bumpy 4x minoL - 1", minos: []testMinoStruct{ 242 | {minoRotation: minos.minoBag[2], x: 0, y: 18}, // minoL 243 | {minoRotation: minos.minoBag[2], x: 5, y: 18}, // minoL 244 | {minoRotation: minos.minoBag[2], x: 10, y: 18}, // minoL 245 | {minoRotation: minos.minoBag[2], x: 15, y: 18}, // minoL 246 | }, fullLines: 0, holes: 0, bumpy: 15, heightEnds: 1}, 247 | {info: "bumpy 4x minoL - 2", minos: []testMinoStruct{ 248 | {minoRotation: minos.minoBag[2], x: 1, y: 18}, // minoL 249 | {minoRotation: minos.minoBag[2], x: 6, y: 18}, // minoL 250 | {minoRotation: minos.minoBag[2], x: 11, y: 18}, // minoL 251 | {minoRotation: minos.minoBag[2], x: 16, y: 18}, // minoL 252 | }, fullLines: 0, holes: 0, bumpy: 16, heightEnds: 0}, 253 | {info: "bumpy 4x minoL - 3", minos: []testMinoStruct{ 254 | {minoRotation: minos.minoBag[2], x: 2, y: 18}, // minoL 255 | {minoRotation: minos.minoBag[2], x: 7, y: 18}, // minoL 256 | {minoRotation: minos.minoBag[2], x: 12, y: 18}, // minoL 257 | {minoRotation: minos.minoBag[2], x: 17, y: 18}, // minoL 258 | }, fullLines: 0, holes: 0, bumpy: 14, heightEnds: 2}, 259 | {info: "bumpy 4x minoL & 10x minoI", minos: []testMinoStruct{ 260 | {minoRotation: minos.minoBag[2], x: 2, y: 18}, // minoL 261 | {minoRotation: minos.minoBag[2], x: 7, y: 18}, // minoL 262 | {minoRotation: minos.minoBag[2], x: 12, y: 18}, // minoL 263 | {minoRotation: minos.minoBag[2], x: 17, y: 18}, // minoL 264 | {minoRotation: minos.minoBag[0], x: 0, y: 16}, // minoI 265 | {minoRotation: minos.minoBag[0], x: 4, y: 16}, // minoI 266 | {minoRotation: minos.minoBag[0], x: 8, y: 16}, // minoI 267 | {minoRotation: minos.minoBag[0], x: 12, y: 16}, // minoI 268 | {minoRotation: minos.minoBag[0], x: 16, y: 16}, // minoI 269 | {minoRotation: minos.minoBag[0], x: 0, y: 15}, // minoI 270 | {minoRotation: minos.minoBag[0], x: 4, y: 15}, // minoI 271 | {minoRotation: minos.minoBag[0], x: 8, y: 15}, // minoI 272 | {minoRotation: minos.minoBag[0], x: 12, y: 15}, // minoI 273 | {minoRotation: minos.minoBag[0], x: 16, y: 15}, // minoI 274 | }, fullLines: 2, holes: 0, bumpy: 14, heightEnds: 2}, 275 | {info: "bumpy 8x minoL & 10x minoI", minos: []testMinoStruct{ 276 | {minoRotation: minos.minoBag[2], x: 2, y: 18}, // minoL 277 | {minoRotation: minos.minoBag[2], x: 7, y: 18}, // minoL 278 | {minoRotation: minos.minoBag[2], x: 12, y: 18}, // minoL 279 | {minoRotation: minos.minoBag[2], x: 17, y: 18}, // minoL 280 | {minoRotation: minos.minoBag[0], x: 0, y: 16}, // minoI 281 | {minoRotation: minos.minoBag[0], x: 4, y: 16}, // minoI 282 | {minoRotation: minos.minoBag[0], x: 8, y: 16}, // minoI 283 | {minoRotation: minos.minoBag[0], x: 12, y: 16}, // minoI 284 | {minoRotation: minos.minoBag[0], x: 16, y: 16}, // minoI 285 | {minoRotation: minos.minoBag[0], x: 0, y: 15}, // minoI 286 | {minoRotation: minos.minoBag[0], x: 4, y: 15}, // minoI 287 | {minoRotation: minos.minoBag[0], x: 8, y: 15}, // minoI 288 | {minoRotation: minos.minoBag[0], x: 12, y: 15}, // minoI 289 | {minoRotation: minos.minoBag[0], x: 16, y: 15}, // minoI 290 | {minoRotation: minos.minoBag[2], x: 2, y: 14}, // minoL 291 | {minoRotation: minos.minoBag[2], x: 7, y: 14}, // minoL 292 | {minoRotation: minos.minoBag[2], x: 12, y: 14}, // minoL 293 | {minoRotation: minos.minoBag[2], x: 17, y: 14}, // minoL 294 | }, fullLines: 2, holes: 8, bumpy: 28, heightEnds: 4}, 295 | } 296 | 297 | runAiTests(t, tests) 298 | } 299 | 300 | func runAiTests(t *testing.T, tests []testAiStruct) { 301 | var mino1 *Mino 302 | var mino2 *Mino 303 | 304 | for _, test := range tests { 305 | board.Clear() 306 | 307 | for i, minoTest := range test.minos { 308 | mino := NewMino() 309 | mino.minoRotation = minoTest.minoRotation 310 | mino.length = len(mino.minoRotation[0]) 311 | mino.x = minoTest.x 312 | mino.y = minoTest.y 313 | if i < len(test.minos)-2 { 314 | mino.SetOnBoard() 315 | } else if i == len(test.minos)-2 { 316 | mino1 = mino 317 | } else { 318 | mino2 = mino 319 | } 320 | } 321 | 322 | fullLines, holes, bumpy, heightEnds := board.boardStatsWithMinos(mino1, mino2) 323 | 324 | if fullLines != test.fullLines { 325 | mino1.SetOnBoard() 326 | lines := board.getDebugBoardWithMino(mino2) 327 | for i := 0; i < len(lines); i++ { 328 | t.Log(lines[i]) 329 | } 330 | t.Errorf("AI fullLines - received: %v - expected: %v - info %v", fullLines, test.fullLines, test.info) 331 | continue 332 | } 333 | if holes != test.holes { 334 | mino1.SetOnBoard() 335 | lines := board.getDebugBoardWithMino(mino2) 336 | for i := 0; i < len(lines); i++ { 337 | t.Log(lines[i]) 338 | } 339 | t.Errorf("AI holes - received: %v - expected: %v - info %v", holes, test.holes, test.info) 340 | continue 341 | } 342 | if bumpy != test.bumpy { 343 | mino1.SetOnBoard() 344 | lines := board.getDebugBoardWithMino(mino2) 345 | for i := 0; i < len(lines); i++ { 346 | t.Log(lines[i]) 347 | } 348 | t.Errorf("AI bumpy - received: %v - expected: %v - info %v", bumpy, test.bumpy, test.info) 349 | continue 350 | } 351 | if heightEnds != test.heightEnds { 352 | mino1.SetOnBoard() 353 | lines := board.getDebugBoardWithMino(mino2) 354 | for i := 0; i < len(lines); i++ { 355 | t.Log(lines[i]) 356 | } 357 | t.Errorf("AI heightEnds - received: %v - expected: %v - info %v", heightEnds, test.heightEnds, test.info) 358 | continue 359 | } 360 | 361 | } 362 | } 363 | -------------------------------------------------------------------------------- /boards.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "io/ioutil" 7 | "os" 8 | 9 | "github.com/gdamore/tcell" 10 | ) 11 | 12 | // loadBoards loads the internal boards 13 | func loadBoards() error { 14 | logger.Println("loadBoards start") 15 | 16 | var boardsJSON []BoardsJSON 17 | err := json.Unmarshal(boardsInternal, &boardsJSON) 18 | if err != nil { 19 | return fmt.Errorf("unmarshal error: %v", err) 20 | } 21 | 22 | numInternalBoards = len(boardsJSON) 23 | boards = make([]Boards, numInternalBoards) 24 | 25 | for boardNum, boardLoad := range boardsJSON { 26 | aBoards := Boards{name: boardLoad.Name} 27 | aBoards.colors = make([][]tcell.Color, len(boardLoad.Mino)) 28 | aBoards.rotation = boardLoad.Rotation 29 | 30 | for i := 0; i < len(boardLoad.Mino); i++ { 31 | aBoards.colors[i] = make([]tcell.Color, len(boardLoad.Mino[i])) 32 | for j := 0; j < len(boardLoad.Mino[i]); j++ { 33 | switch boardLoad.Mino[i][j] { 34 | case "b": 35 | aBoards.colors[i][j] = colorBlank 36 | case "i": 37 | aBoards.colors[i][j] = colorCyan 38 | case "j": 39 | aBoards.colors[i][j] = colorBlue 40 | case "l": 41 | aBoards.colors[i][j] = colorWhite 42 | case "o": 43 | aBoards.colors[i][j] = colorYellow 44 | case "s": 45 | aBoards.colors[i][j] = colorGreen 46 | case "t": 47 | aBoards.colors[i][j] = colorMagenta 48 | case "z": 49 | aBoards.colors[i][j] = colorRed 50 | } 51 | } 52 | } 53 | boards[boardNum] = aBoards 54 | } 55 | 56 | boardsJSON = nil 57 | 58 | logger.Println("loadBoards end") 59 | 60 | return nil 61 | } 62 | 63 | // loadUserBoards loads the user boards from file 64 | func loadUserBoards() error { 65 | _, err := os.Stat(baseDir + settingsFileName) 66 | if os.IsNotExist(err) { 67 | return nil 68 | } 69 | 70 | logger.Println("loadUserBoards start") 71 | 72 | if err != nil { 73 | return fmt.Errorf("stat error: %v", err) 74 | } 75 | 76 | var settingsJSON []byte 77 | settingsJSON, err = ioutil.ReadFile(baseDir + settingsFileName) 78 | if err != nil { 79 | return fmt.Errorf("read file error: %v", err) 80 | } 81 | if len(settingsJSON) < 2 { 82 | return nil 83 | } 84 | 85 | var settings Settings 86 | err = json.Unmarshal(settingsJSON, &settings) 87 | if err != nil { 88 | return fmt.Errorf("unmarshal error: %v", err) 89 | } 90 | 91 | for _, boardLoad := range settings.Boards { 92 | aBoards := Boards{name: boardLoad.Name} 93 | aBoards.colors = make([][]tcell.Color, len(boardLoad.Mino)) 94 | aBoards.rotation = boardLoad.Rotation 95 | 96 | for i := 0; i < len(boardLoad.Mino); i++ { 97 | aBoards.colors[i] = make([]tcell.Color, len(boardLoad.Mino[i])) 98 | for j := 0; j < len(boardLoad.Mino[i]); j++ { 99 | switch boardLoad.Mino[i][j] { 100 | case "b": 101 | aBoards.colors[i][j] = colorBlank 102 | case "i": 103 | aBoards.colors[i][j] = colorCyan 104 | case "j": 105 | aBoards.colors[i][j] = colorBlue 106 | case "l": 107 | aBoards.colors[i][j] = colorWhite 108 | case "o": 109 | aBoards.colors[i][j] = colorYellow 110 | case "s": 111 | aBoards.colors[i][j] = colorGreen 112 | case "t": 113 | aBoards.colors[i][j] = colorMagenta 114 | case "z": 115 | aBoards.colors[i][j] = colorRed 116 | } 117 | } 118 | } 119 | 120 | boards = append(boards, aBoards) 121 | } 122 | 123 | logger.Println("loadUserBoards end") 124 | 125 | return nil 126 | } 127 | 128 | // saveUserBoards saves the user boards to file 129 | func saveUserBoards() error { 130 | logger.Println("saveUserBoards start") 131 | 132 | boardsJSON := make([]BoardsJSON, len(boards)-numInternalBoards) 133 | 134 | for x := numInternalBoards; x < len(boards); x++ { 135 | aBoards := BoardsJSON{Name: boards[x].name} 136 | aBoards.Mino = make([][]string, len(boards[x].colors)) 137 | aBoards.Rotation = boards[x].rotation 138 | 139 | for i := 0; i < len(boards[x].colors); i++ { 140 | aBoards.Mino[i] = make([]string, len(boards[x].colors[i])) 141 | for j := 0; j < len(boards[x].colors[i]); j++ { 142 | switch boards[x].colors[i][j] { 143 | case colorBlank: 144 | aBoards.Mino[i][j] = "b" 145 | case colorCyan: 146 | aBoards.Mino[i][j] = "i" 147 | case colorBlue: 148 | aBoards.Mino[i][j] = "j" 149 | case colorWhite: 150 | aBoards.Mino[i][j] = "l" 151 | case colorYellow: 152 | aBoards.Mino[i][j] = "o" 153 | case colorGreen: 154 | aBoards.Mino[i][j] = "s" 155 | case colorMagenta: 156 | aBoards.Mino[i][j] = "t" 157 | case colorRed: 158 | aBoards.Mino[i][j] = "z" 159 | } 160 | } 161 | } 162 | 163 | boardsJSON[x-numInternalBoards] = aBoards 164 | } 165 | 166 | settings := Settings{Boards: boardsJSON} 167 | 168 | settingsJSON, err := json.Marshal(settings) 169 | if err != nil { 170 | return fmt.Errorf("marshal error: %v", err) 171 | } 172 | 173 | err = ioutil.WriteFile(baseDir+settingsFileName, settingsJSON, 0644) 174 | if err != nil { 175 | return fmt.Errorf("write file error: %v", err) 176 | } 177 | 178 | logger.Println("saveUserBoards end") 179 | 180 | return nil 181 | } 182 | 183 | var boardsInternal = []byte(` 184 | [ 185 | { 186 | "name":"10 x 20 blank", 187 | "mino":[ 188 | ["b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b"], 189 | ["b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b"], 190 | ["b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b"], 191 | ["b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b"], 192 | ["b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b"], 193 | ["b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b"], 194 | ["b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b"], 195 | ["b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b"], 196 | ["b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b"], 197 | ["b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b"] 198 | ], 199 | "rotation":[ 200 | [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], 201 | [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], 202 | [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], 203 | [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], 204 | [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], 205 | [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], 206 | [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], 207 | [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], 208 | [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], 209 | [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0] 210 | ] 211 | }, 212 | { 213 | "name":"10 x 20 checkerboard double", 214 | "mino":[ 215 | ["b","b","b","b","b","b","z","z","b","b","z","z","b","b","z","z","b","b","z","z"], 216 | ["b","b","b","b","b","b","z","z","b","b","z","z","b","b","z","z","b","b","z","z"], 217 | ["b","b","b","b","z","z","b","b","z","z","b","b","z","z","b","b","z","z","b","b"], 218 | ["b","b","b","b","z","z","b","b","z","z","b","b","z","z","b","b","z","z","b","b"], 219 | ["b","b","b","b","b","b","z","z","b","b","z","z","b","b","z","z","b","b","z","z"], 220 | ["b","b","b","b","b","b","z","z","b","b","z","z","b","b","z","z","b","b","z","z"], 221 | ["b","b","b","b","z","z","b","b","z","z","b","b","z","z","b","b","z","z","b","b"], 222 | ["b","b","b","b","z","z","b","b","z","z","b","b","z","z","b","b","z","z","b","b"], 223 | ["b","b","b","b","b","b","z","z","b","b","z","z","b","b","z","z","b","b","z","z"], 224 | ["b","b","b","b","b","b","z","z","b","b","z","z","b","b","z","z","b","b","z","z"] 225 | ], 226 | "rotation":[ 227 | [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], 228 | [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], 229 | [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], 230 | [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], 231 | [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], 232 | [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], 233 | [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], 234 | [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], 235 | [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], 236 | [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0] 237 | ] 238 | }, 239 | { 240 | "name":"10 x 20 checkerboard single", 241 | "mino":[ 242 | ["b","b","b","b","b","z","b","z","b","z","b","z","b","z","b","z","b","z","b","z"], 243 | ["b","b","b","b","b","b","z","b","z","b","z","b","z","b","z","b","z","b","z","b"], 244 | ["b","b","b","b","b","z","b","z","b","z","b","z","b","z","b","z","b","z","b","z"], 245 | ["b","b","b","b","b","b","z","b","z","b","z","b","z","b","z","b","z","b","z","b"], 246 | ["b","b","b","b","b","z","b","z","b","z","b","z","b","z","b","z","b","z","b","z"], 247 | ["b","b","b","b","b","b","z","b","z","b","z","b","z","b","z","b","z","b","z","b"], 248 | ["b","b","b","b","b","z","b","z","b","z","b","z","b","z","b","z","b","z","b","z"], 249 | ["b","b","b","b","b","b","z","b","z","b","z","b","z","b","z","b","z","b","z","b"], 250 | ["b","b","b","b","b","z","b","z","b","z","b","z","b","z","b","z","b","z","b","z"], 251 | ["b","b","b","b","b","b","z","b","z","b","z","b","z","b","z","b","z","b","z","b"] 252 | ], 253 | "rotation":[ 254 | [0,0,0,0,0,2,0,2,0,2,0,2,0,2,0,2,0,2,0,2], 255 | [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], 256 | [0,0,0,0,0,2,0,2,0,2,0,2,0,2,0,2,0,2,0,2], 257 | [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], 258 | [0,0,0,0,0,2,0,2,0,2,0,2,0,2,0,2,0,2,0,2], 259 | [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], 260 | [0,0,0,0,0,2,0,2,0,2,0,2,0,2,0,2,0,2,0,2], 261 | [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], 262 | [0,0,0,0,0,2,0,2,0,2,0,2,0,2,0,2,0,2,0,2], 263 | [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0] 264 | ] 265 | }, 266 | { 267 | "name":"20 x 20 blank", 268 | "mino":[ 269 | ["b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b"], 270 | ["b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b"], 271 | ["b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b"], 272 | ["b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b"], 273 | ["b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b"], 274 | ["b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b"], 275 | ["b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b"], 276 | ["b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b"], 277 | ["b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b"], 278 | ["b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b"], 279 | ["b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b"], 280 | ["b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b"], 281 | ["b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b"], 282 | ["b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b"], 283 | ["b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b"], 284 | ["b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b"], 285 | ["b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b"], 286 | ["b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b"], 287 | ["b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b"], 288 | ["b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b"] 289 | ], 290 | "rotation":[ 291 | [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], 292 | [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], 293 | [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], 294 | [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], 295 | [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], 296 | [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], 297 | [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], 298 | [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], 299 | [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], 300 | [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], 301 | [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], 302 | [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], 303 | [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], 304 | [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], 305 | [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], 306 | [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], 307 | [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], 308 | [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], 309 | [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], 310 | [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0] 311 | ] 312 | }, 313 | { 314 | "name":"20 x 20 heart", 315 | "mino":[ 316 | ["b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b"], 317 | ["b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b"], 318 | ["b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b"], 319 | ["b","b","b","b","b","b","b","b","b","l","l","l","b","b","b","b","b","b","b","b"], 320 | ["b","b","b","b","b","b","b","b","z","z","t","l","o","o","b","b","b","b","b","b"], 321 | ["b","b","b","b","b","b","b","z","z","t","t","t","o","o","j","b","b","b","b","b"], 322 | ["b","b","b","b","b","b","b","l","l","l","i","i","i","i","j","b","b","b","b","b"], 323 | ["b","b","b","b","b","b","b","s","s","l","l","l","z","z","j","j","b","b","b","b"], 324 | ["b","b","b","b","b","b","b","b","s","s","l","z","z","i","i","i","i","b","b","b"], 325 | ["b","b","b","b","b","b","b","b","j","j","l","s","s","t","t","t","z","z","b","b"], 326 | ["b","b","b","b","b","b","b","b","s","j","o","o","s","s","t","z","z","b","b","b"], 327 | ["b","b","b","b","b","b","b","s","s","j","o","o","o","o","l","l","b","b","b","b"], 328 | ["b","b","b","b","b","b","b","s","z","z","s","s","o","o","l","b","b","b","b","b"], 329 | ["b","b","b","b","b","b","b","z","z","t","j","s","s","j","l","b","b","b","b","b"], 330 | ["b","b","b","b","b","b","b","b","t","t","j","j","j","j","b","b","b","b","b","b"], 331 | ["b","b","b","b","b","b","b","b","b","t","j","j","b","b","b","b","b","b","b","b"], 332 | ["b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b"], 333 | ["b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b"], 334 | ["b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b"], 335 | ["b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b"] 336 | ], 337 | "rotation":[ 338 | [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], 339 | [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], 340 | [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], 341 | [0,0,0,0,0,0,0,0,0,1,1,1,0,0,0,0,0,0,0,0], 342 | [0,0,0,0,0,0,0,0,1,1,3,1,0,0,0,0,0,0,0,0], 343 | [0,0,0,0,0,0,0,1,1,3,3,3,0,0,2,0,0,0,0,0], 344 | [0,0,0,0,0,0,0,1,1,1,3,3,3,3,2,0,0,0,0,0], 345 | [0,0,0,0,0,0,0,1,1,1,2,2,1,1,2,2,0,0,0,0], 346 | [0,0,0,0,0,0,0,0,1,1,2,1,1,1,1,1,1,0,0,0], 347 | [0,0,0,0,0,0,0,0,0,0,2,1,1,1,1,1,1,1,0,0], 348 | [0,0,0,0,0,0,0,0,0,0,1,1,1,1,1,1,1,0,0,0], 349 | [0,0,0,0,0,0,0,0,0,0,1,1,3,3,2,2,0,0,0,0], 350 | [0,0,0,0,0,0,0,0,1,1,1,1,3,3,2,0,0,0,0,0], 351 | [0,0,0,0,0,0,0,1,1,0,2,1,1,1,2,0,0,0,0,0], 352 | [0,0,0,0,0,0,0,0,0,0,2,3,3,3,0,0,0,0,0,0], 353 | [0,0,0,0,0,0,0,0,0,0,2,2,0,0,0,0,0,0,0,0], 354 | [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], 355 | [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], 356 | [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], 357 | [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0] 358 | ] 359 | }, 360 | { 361 | "name":"20 x 20 checkerboard double", 362 | "mino":[ 363 | ["b","b","b","b","b","b","z","z","b","b","z","z","b","b","z","z","b","b","z","z"], 364 | ["b","b","b","b","b","b","z","z","b","b","z","z","b","b","z","z","b","b","z","z"], 365 | ["b","b","b","b","z","z","b","b","z","z","b","b","z","z","b","b","z","z","b","b"], 366 | ["b","b","b","b","z","z","b","b","z","z","b","b","z","z","b","b","z","z","b","b"], 367 | ["b","b","b","b","b","b","z","z","b","b","z","z","b","b","z","z","b","b","z","z"], 368 | ["b","b","b","b","b","b","z","z","b","b","z","z","b","b","z","z","b","b","z","z"], 369 | ["b","b","b","b","z","z","b","b","z","z","b","b","z","z","b","b","z","z","b","b"], 370 | ["b","b","b","b","z","z","b","b","z","z","b","b","z","z","b","b","z","z","b","b"], 371 | ["b","b","b","b","b","b","z","z","b","b","z","z","b","b","z","z","b","b","z","z"], 372 | ["b","b","b","b","b","b","z","z","b","b","z","z","b","b","z","z","b","b","z","z"], 373 | ["b","b","b","b","z","z","b","b","z","z","b","b","z","z","b","b","z","z","b","b"], 374 | ["b","b","b","b","z","z","b","b","z","z","b","b","z","z","b","b","z","z","b","b"], 375 | ["b","b","b","b","b","b","z","z","b","b","z","z","b","b","z","z","b","b","z","z"], 376 | ["b","b","b","b","b","b","z","z","b","b","z","z","b","b","z","z","b","b","z","z"], 377 | ["b","b","b","b","z","z","b","b","z","z","b","b","z","z","b","b","z","z","b","b"], 378 | ["b","b","b","b","z","z","b","b","z","z","b","b","z","z","b","b","z","z","b","b"], 379 | ["b","b","b","b","b","b","z","z","b","b","z","z","b","b","z","z","b","b","z","z"], 380 | ["b","b","b","b","b","b","z","z","b","b","z","z","b","b","z","z","b","b","z","z"], 381 | ["b","b","b","b","z","z","b","b","z","z","b","b","z","z","b","b","z","z","b","b"], 382 | ["b","b","b","b","z","z","b","b","z","z","b","b","z","z","b","b","z","z","b","b"] 383 | ], 384 | "rotation":[ 385 | [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], 386 | [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], 387 | [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], 388 | [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], 389 | [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], 390 | [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], 391 | [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], 392 | [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], 393 | [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], 394 | [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], 395 | [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], 396 | [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], 397 | [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], 398 | [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], 399 | [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], 400 | [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], 401 | [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], 402 | [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], 403 | [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], 404 | [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0] 405 | ] 406 | }, 407 | { 408 | "name":"20 x 20 checkerboard single", 409 | "mino":[ 410 | ["b","b","b","b","b","z","b","z","b","z","b","z","b","z","b","z","b","z","b","z"], 411 | ["b","b","b","b","b","b","z","b","z","b","z","b","z","b","z","b","z","b","z","b"], 412 | ["b","b","b","b","b","z","b","z","b","z","b","z","b","z","b","z","b","z","b","z"], 413 | ["b","b","b","b","b","b","z","b","z","b","z","b","z","b","z","b","z","b","z","b"], 414 | ["b","b","b","b","b","z","b","z","b","z","b","z","b","z","b","z","b","z","b","z"], 415 | ["b","b","b","b","b","b","z","b","z","b","z","b","z","b","z","b","z","b","z","b"], 416 | ["b","b","b","b","b","z","b","z","b","z","b","z","b","z","b","z","b","z","b","z"], 417 | ["b","b","b","b","b","b","z","b","z","b","z","b","z","b","z","b","z","b","z","b"], 418 | ["b","b","b","b","b","z","b","z","b","z","b","z","b","z","b","z","b","z","b","z"], 419 | ["b","b","b","b","b","b","z","b","z","b","z","b","z","b","z","b","z","b","z","b"], 420 | ["b","b","b","b","b","z","b","z","b","z","b","z","b","z","b","z","b","z","b","z"], 421 | ["b","b","b","b","b","b","z","b","z","b","z","b","z","b","z","b","z","b","z","b"], 422 | ["b","b","b","b","b","z","b","z","b","z","b","z","b","z","b","z","b","z","b","z"], 423 | ["b","b","b","b","b","b","z","b","z","b","z","b","z","b","z","b","z","b","z","b"], 424 | ["b","b","b","b","b","z","b","z","b","z","b","z","b","z","b","z","b","z","b","z"], 425 | ["b","b","b","b","b","b","z","b","z","b","z","b","z","b","z","b","z","b","z","b"], 426 | ["b","b","b","b","b","z","b","z","b","z","b","z","b","z","b","z","b","z","b","z"], 427 | ["b","b","b","b","b","b","z","b","z","b","z","b","z","b","z","b","z","b","z","b"], 428 | ["b","b","b","b","b","z","b","z","b","z","b","z","b","z","b","z","b","z","b","z"], 429 | ["b","b","b","b","b","b","z","b","z","b","z","b","z","b","z","b","z","b","z","b"] 430 | ], 431 | "rotation":[ 432 | [0,0,0,0,0,2,0,2,0,2,0,2,0,2,0,2,0,2,0,2], 433 | [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], 434 | [0,0,0,0,0,2,0,2,0,2,0,2,0,2,0,2,0,2,0,2], 435 | [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], 436 | [0,0,0,0,0,2,0,2,0,2,0,2,0,2,0,2,0,2,0,2], 437 | [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], 438 | [0,0,0,0,0,2,0,2,0,2,0,2,0,2,0,2,0,2,0,2], 439 | [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], 440 | [0,0,0,0,0,2,0,2,0,2,0,2,0,2,0,2,0,2,0,2], 441 | [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], 442 | [0,0,0,0,0,2,0,2,0,2,0,2,0,2,0,2,0,2,0,2], 443 | [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], 444 | [0,0,0,0,0,2,0,2,0,2,0,2,0,2,0,2,0,2,0,2], 445 | [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], 446 | [0,0,0,0,0,2,0,2,0,2,0,2,0,2,0,2,0,2,0,2], 447 | [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], 448 | [0,0,0,0,0,2,0,2,0,2,0,2,0,2,0,2,0,2,0,2], 449 | [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], 450 | [0,0,0,0,0,2,0,2,0,2,0,2,0,2,0,2,0,2,0,2], 451 | [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0] 452 | ] 453 | }, 454 | { 455 | "name":"26 x 28 peace symbol", 456 | "Mino":[ 457 | ["b","b","b","b","b","b","b","b","b","b","b","b","j","i","i","i","i","o","o","l","l","j","j","j","z","j","j","i"], 458 | ["b","b","b","b","b","b","b","b","j","l","j","j","j","s","j","j","j","o","o","l","z","j","l","l","z","z","j","i"], 459 | ["b","b","b","b","b","b","b","b","j","l","l","l","s","s","j","b","b","b","b","l","z","z","l","o","o","z","j","i"], 460 | ["b","b","b","b","b","b","b","t","j","j","z","z","s","b","b","b","j","l","b","b","b","z","l","o","o","j","z","i"], 461 | ["b","b","b","b","b","b","t","t","t","z","z","b","b","b","j","j","j","l","l","l","b","b","b","j","j","j","z","z"], 462 | ["b","b","b","b","b","b","i","i","i","i","b","b","j","z","s","s","t","t","t","z","z","s","b","b","t","t","t","z"], 463 | ["b","b","b","b","b","b","t","t","t","b","b","s","j","z","z","s","s","t","z","z","s","s","b","b","b","t","l","l"], 464 | ["b","b","b","b","b","b","b","t","b","b","s","s","j","j","z","j","j","j","o","o","s","b","b","s","b","b","l","i"], 465 | ["b","b","b","b","b","b","z","z","b","l","s","i","i","i","i","j","s","s","o","o","b","b","s","s","j","b","l","i"], 466 | ["b","b","b","b","b","z","z","b","b","l","o","o","l","l","t","t","t","s","s","b","b","z","s","b","j","b","b","i"], 467 | ["b","b","b","b","b","l","l","b","l","l","o","o","l","o","o","t","z","z","b","b","t","z","z","l","j","j","b","i"], 468 | ["b","b","b","b","b","l","b","b","i","i","i","i","l","o","o","z","z","b","b","t","t","t","z","l","l","l","b","b"], 469 | ["b","b","b","b","b","l","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b"], 470 | ["b","b","b","b","b","j","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b"], 471 | ["b","b","b","b","b","j","b","b","o","o","l","l","t","t","t","z","z","b","b","i","i","i","i","j","o","o","b","b"], 472 | ["b","b","b","b","b","j","j","b","o","o","l","s","s","t","z","z","s","s","b","b","s","s","b","j","o","o","b","l"], 473 | ["b","b","b","b","b","b","z","b","b","z","l","i","s","s","l","l","l","s","s","b","b","s","s","j","j","b","b","l"], 474 | ["b","b","b","b","b","b","z","z","b","z","z","i","j","j","j","i","l","j","j","j","b","b","t","t","t","b","l","l"], 475 | ["b","b","b","b","b","b","l","z","b","b","z","i","j","o","o","i","s","j","o","o","z","b","b","t","b","b","j","j"], 476 | ["b","b","b","b","b","b","l","l","l","b","b","i","l","o","o","i","s","s","o","o","z","z","b","b","b","o","o","j"], 477 | ["b","b","b","b","b","b","i","i","i","i","b","b","l","l","l","i","j","s","t","t","t","z","b","b","t","o","o","j"], 478 | ["b","b","b","b","b","b","b","b","t","s","s","b","b","b","j","j","j","z","z","t","b","b","b","t","t","t","z","z"], 479 | ["b","b","b","b","b","b","b","b","t","t","s","s","l","b","b","b","z","z","b","b","b","i","i","i","i","z","z","i"], 480 | ["b","b","b","b","b","b","b","b","t","o","o","z","l","l","l","b","b","b","b","j","j","j","t","t","t","j","j","i"], 481 | ["b","b","b","b","b","b","b","b","b","o","o","z","z","j","j","j","l","l","l","j","t","s","s","t","o","o","j","i"], 482 | ["b","b","b","b","b","b","b","b","i","i","i","i","z","j","i","i","i","i","l","t","t","t","s","s","o","o","j","i"] 483 | ], 484 | "Rotation":[ 485 | [0,0,0,0,0,0,0,0,0,0,0,0,3,3,3,3,3,0,0,2,2,1,1,1,0,0,0,2], 486 | [0,0,0,0,0,0,0,0,2,3,3,3,3,0,1,1,1,0,0,2,0,1,2,2,0,0,0,2], 487 | [0,0,0,0,0,0,0,0,2,3,3,3,0,0,1,0,0,0,0,2,0,0,2,1,1,0,0,2], 488 | [0,0,0,0,0,0,0,3,2,2,1,1,0,0,0,0,3,3,0,0,0,0,2,1,1,3,0,2], 489 | [0,0,0,0,0,0,3,3,3,1,1,0,0,0,3,3,3,3,3,3,0,0,0,3,3,3,0,0], 490 | [0,0,0,0,0,0,1,1,1,1,0,0,2,0,3,3,1,1,1,3,3,0,0,0,1,1,1,0], 491 | [0,0,0,0,0,0,1,1,1,0,0,2,2,0,0,3,3,1,3,3,0,0,0,0,0,1,2,2], 492 | [0,0,0,0,0,0,0,1,0,0,2,2,2,2,0,1,1,1,2,2,0,0,0,2,0,0,2,0], 493 | [0,0,0,0,0,0,3,3,0,0,2,3,3,3,3,1,1,1,2,2,0,0,2,2,2,0,2,0], 494 | [0,0,0,0,0,3,3,0,0,0,1,1,2,2,1,1,1,1,1,0,0,0,2,0,2,0,0,0], 495 | [0,0,0,0,0,2,2,0,0,0,1,1,2,3,3,1,3,3,0,0,3,0,0,3,2,2,0,0], 496 | [0,0,0,0,0,2,0,0,1,1,1,1,2,3,3,3,3,0,0,3,3,3,0,3,3,3,0,0], 497 | [0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], 498 | [0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], 499 | [0,0,0,0,0,2,0,0,0,0,2,2,1,1,1,3,3,0,0,1,1,1,1,2,1,1,0,0], 500 | [0,0,0,0,0,2,2,0,0,0,2,1,1,1,3,3,1,1,0,0,3,3,0,2,1,1,0,0], 501 | [0,0,0,0,0,0,2,0,0,0,2,2,1,1,1,1,1,1,1,0,0,3,3,2,2,0,0,0], 502 | [0,0,0,0,0,0,2,2,0,0,0,2,1,1,1,0,1,1,1,1,0,0,1,1,1,0,0,0], 503 | [0,0,0,0,0,0,3,2,0,0,0,2,1,3,3,0,0,1,0,0,2,0,0,1,0,0,0,0], 504 | [0,0,0,0,0,0,3,3,3,0,0,2,3,3,3,0,0,0,0,0,2,2,0,0,0,2,2,0], 505 | [0,0,0,0,0,0,1,1,1,1,0,0,3,3,3,0,3,0,1,1,1,2,0,0,3,2,2,0], 506 | [0,0,0,0,0,0,0,0,2,3,3,0,0,0,3,3,3,3,3,1,0,0,0,3,3,3,1,1], 507 | [0,0,0,0,0,0,0,0,2,2,3,3,3,0,0,0,3,3,0,0,0,3,3,3,3,1,1,2], 508 | [0,0,0,0,0,0,0,0,2,1,1,2,3,3,3,0,0,0,0,1,1,1,1,1,1,0,0,2], 509 | [0,0,0,0,0,0,0,0,0,1,1,2,2,1,1,1,1,1,1,1,3,3,3,1,3,3,0,2], 510 | [0,0,0,0,0,0,0,0,3,3,3,3,2,1,3,3,3,3,1,3,3,3,3,3,3,3,0,2] 511 | ] 512 | }, 513 | { 514 | "name":"30 x 30 blank", 515 | "mino":[ 516 | ["b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b"], 517 | ["b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b"], 518 | ["b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b"], 519 | ["b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b"], 520 | ["b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b"], 521 | ["b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b"], 522 | ["b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b"], 523 | ["b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b"], 524 | ["b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b"], 525 | ["b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b"], 526 | ["b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b"], 527 | ["b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b"], 528 | ["b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b"], 529 | ["b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b"], 530 | ["b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b"], 531 | ["b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b"], 532 | ["b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b"], 533 | ["b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b"], 534 | ["b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b"], 535 | ["b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b"], 536 | ["b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b"], 537 | ["b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b"], 538 | ["b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b"], 539 | ["b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b"], 540 | ["b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b"], 541 | ["b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b"], 542 | ["b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b"], 543 | ["b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b"], 544 | ["b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b"], 545 | ["b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b","b"] 546 | ], 547 | "rotation":[ 548 | [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], 549 | [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], 550 | [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], 551 | [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], 552 | [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], 553 | [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], 554 | [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], 555 | [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], 556 | [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], 557 | [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], 558 | [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], 559 | [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], 560 | [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], 561 | [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], 562 | [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], 563 | [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], 564 | [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], 565 | [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], 566 | [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], 567 | [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], 568 | [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], 569 | [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], 570 | [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], 571 | [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], 572 | [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], 573 | [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], 574 | [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], 575 | [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], 576 | [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], 577 | [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0] 578 | ] 579 | }, 580 | { 581 | "name":"30 x 30 checkerboard double", 582 | "mino":[ 583 | ["b","b","b","b","b","b","b","b","z","z","b","b","z","z","b","b","z","z","b","b","z","z","b","b","z","z","b","b","z","z"], 584 | ["b","b","b","b","b","b","b","b","z","z","b","b","z","z","b","b","z","z","b","b","z","z","b","b","z","z","b","b","z","z"], 585 | ["b","b","b","b","b","b","z","z","b","b","z","z","b","b","z","z","b","b","z","z","b","b","z","z","b","b","z","z","b","b"], 586 | ["b","b","b","b","b","b","z","z","b","b","z","z","b","b","z","z","b","b","z","z","b","b","z","z","b","b","z","z","b","b"], 587 | ["b","b","b","b","b","b","b","b","z","z","b","b","z","z","b","b","z","z","b","b","z","z","b","b","z","z","b","b","z","z"], 588 | ["b","b","b","b","b","b","b","b","z","z","b","b","z","z","b","b","z","z","b","b","z","z","b","b","z","z","b","b","z","z"], 589 | ["b","b","b","b","b","b","z","z","b","b","z","z","b","b","z","z","b","b","z","z","b","b","z","z","b","b","z","z","b","b"], 590 | ["b","b","b","b","b","b","z","z","b","b","z","z","b","b","z","z","b","b","z","z","b","b","z","z","b","b","z","z","b","b"], 591 | ["b","b","b","b","b","b","b","b","z","z","b","b","z","z","b","b","z","z","b","b","z","z","b","b","z","z","b","b","z","z"], 592 | ["b","b","b","b","b","b","b","b","z","z","b","b","z","z","b","b","z","z","b","b","z","z","b","b","z","z","b","b","z","z"], 593 | ["b","b","b","b","b","b","z","z","b","b","z","z","b","b","z","z","b","b","z","z","b","b","z","z","b","b","z","z","b","b"], 594 | ["b","b","b","b","b","b","z","z","b","b","z","z","b","b","z","z","b","b","z","z","b","b","z","z","b","b","z","z","b","b"], 595 | ["b","b","b","b","b","b","b","b","z","z","b","b","z","z","b","b","z","z","b","b","z","z","b","b","z","z","b","b","z","z"], 596 | ["b","b","b","b","b","b","b","b","z","z","b","b","z","z","b","b","z","z","b","b","z","z","b","b","z","z","b","b","z","z"], 597 | ["b","b","b","b","b","b","z","z","b","b","z","z","b","b","z","z","b","b","z","z","b","b","z","z","b","b","z","z","b","b"], 598 | ["b","b","b","b","b","b","z","z","b","b","z","z","b","b","z","z","b","b","z","z","b","b","z","z","b","b","z","z","b","b"], 599 | ["b","b","b","b","b","b","b","b","z","z","b","b","z","z","b","b","z","z","b","b","z","z","b","b","z","z","b","b","z","z"], 600 | ["b","b","b","b","b","b","b","b","z","z","b","b","z","z","b","b","z","z","b","b","z","z","b","b","z","z","b","b","z","z"], 601 | ["b","b","b","b","b","b","z","z","b","b","z","z","b","b","z","z","b","b","z","z","b","b","z","z","b","b","z","z","b","b"], 602 | ["b","b","b","b","b","b","z","z","b","b","z","z","b","b","z","z","b","b","z","z","b","b","z","z","b","b","z","z","b","b"], 603 | ["b","b","b","b","b","b","b","b","z","z","b","b","z","z","b","b","z","z","b","b","z","z","b","b","z","z","b","b","z","z"], 604 | ["b","b","b","b","b","b","b","b","z","z","b","b","z","z","b","b","z","z","b","b","z","z","b","b","z","z","b","b","z","z"], 605 | ["b","b","b","b","b","b","z","z","b","b","z","z","b","b","z","z","b","b","z","z","b","b","z","z","b","b","z","z","b","b"], 606 | ["b","b","b","b","b","b","z","z","b","b","z","z","b","b","z","z","b","b","z","z","b","b","z","z","b","b","z","z","b","b"], 607 | ["b","b","b","b","b","b","b","b","z","z","b","b","z","z","b","b","z","z","b","b","z","z","b","b","z","z","b","b","z","z"], 608 | ["b","b","b","b","b","b","b","b","z","z","b","b","z","z","b","b","z","z","b","b","z","z","b","b","z","z","b","b","z","z"], 609 | ["b","b","b","b","b","b","z","z","b","b","z","z","b","b","z","z","b","b","z","z","b","b","z","z","b","b","z","z","b","b"], 610 | ["b","b","b","b","b","b","z","z","b","b","z","z","b","b","z","z","b","b","z","z","b","b","z","z","b","b","z","z","b","b"], 611 | ["b","b","b","b","b","b","b","b","z","z","b","b","z","z","b","b","z","z","b","b","z","z","b","b","z","z","b","b","z","z"], 612 | ["b","b","b","b","b","b","b","b","z","z","b","b","z","z","b","b","z","z","b","b","z","z","b","b","z","z","b","b","z","z"] 613 | ], 614 | "rotation":[ 615 | [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], 616 | [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], 617 | [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], 618 | [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], 619 | [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], 620 | [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], 621 | [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], 622 | [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], 623 | [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], 624 | [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], 625 | [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], 626 | [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], 627 | [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], 628 | [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], 629 | [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], 630 | [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], 631 | [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], 632 | [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], 633 | [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], 634 | [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], 635 | [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], 636 | [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], 637 | [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], 638 | [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], 639 | [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], 640 | [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], 641 | [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], 642 | [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], 643 | [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], 644 | [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0] 645 | ] 646 | }, 647 | { 648 | "name":"30 x 30 checkerboard single", 649 | "mino":[ 650 | ["b","b","b","b","b","b","b","z","b","z","b","z","b","z","b","z","b","z","b","z","b","z","b","z","b","z","b","z","b","z"], 651 | ["b","b","b","b","b","b","z","b","z","b","z","b","z","b","z","b","z","b","z","b","z","b","z","b","z","b","z","b","z","b"], 652 | ["b","b","b","b","b","b","b","z","b","z","b","z","b","z","b","z","b","z","b","z","b","z","b","z","b","z","b","z","b","z"], 653 | ["b","b","b","b","b","b","z","b","z","b","z","b","z","b","z","b","z","b","z","b","z","b","z","b","z","b","z","b","z","b"], 654 | ["b","b","b","b","b","b","b","z","b","z","b","z","b","z","b","z","b","z","b","z","b","z","b","z","b","z","b","z","b","z"], 655 | ["b","b","b","b","b","b","z","b","z","b","z","b","z","b","z","b","z","b","z","b","z","b","z","b","z","b","z","b","z","b"], 656 | ["b","b","b","b","b","b","b","z","b","z","b","z","b","z","b","z","b","z","b","z","b","z","b","z","b","z","b","z","b","z"], 657 | ["b","b","b","b","b","b","z","b","z","b","z","b","z","b","z","b","z","b","z","b","z","b","z","b","z","b","z","b","z","b"], 658 | ["b","b","b","b","b","b","b","z","b","z","b","z","b","z","b","z","b","z","b","z","b","z","b","z","b","z","b","z","b","z"], 659 | ["b","b","b","b","b","b","z","b","z","b","z","b","z","b","z","b","z","b","z","b","z","b","z","b","z","b","z","b","z","b"], 660 | ["b","b","b","b","b","b","b","z","b","z","b","z","b","z","b","z","b","z","b","z","b","z","b","z","b","z","b","z","b","z"], 661 | ["b","b","b","b","b","b","z","b","z","b","z","b","z","b","z","b","z","b","z","b","z","b","z","b","z","b","z","b","z","b"], 662 | ["b","b","b","b","b","b","b","z","b","z","b","z","b","z","b","z","b","z","b","z","b","z","b","z","b","z","b","z","b","z"], 663 | ["b","b","b","b","b","b","z","b","z","b","z","b","z","b","z","b","z","b","z","b","z","b","z","b","z","b","z","b","z","b"], 664 | ["b","b","b","b","b","b","b","z","b","z","b","z","b","z","b","z","b","z","b","z","b","z","b","z","b","z","b","z","b","z"], 665 | ["b","b","b","b","b","b","z","b","z","b","z","b","z","b","z","b","z","b","z","b","z","b","z","b","z","b","z","b","z","b"], 666 | ["b","b","b","b","b","b","b","z","b","z","b","z","b","z","b","z","b","z","b","z","b","z","b","z","b","z","b","z","b","z"], 667 | ["b","b","b","b","b","b","z","b","z","b","z","b","z","b","z","b","z","b","z","b","z","b","z","b","z","b","z","b","z","b"], 668 | ["b","b","b","b","b","b","b","z","b","z","b","z","b","z","b","z","b","z","b","z","b","z","b","z","b","z","b","z","b","z"], 669 | ["b","b","b","b","b","b","z","b","z","b","z","b","z","b","z","b","z","b","z","b","z","b","z","b","z","b","z","b","z","b"], 670 | ["b","b","b","b","b","b","b","z","b","z","b","z","b","z","b","z","b","z","b","z","b","z","b","z","b","z","b","z","b","z"], 671 | ["b","b","b","b","b","b","z","b","z","b","z","b","z","b","z","b","z","b","z","b","z","b","z","b","z","b","z","b","z","b"], 672 | ["b","b","b","b","b","b","b","z","b","z","b","z","b","z","b","z","b","z","b","z","b","z","b","z","b","z","b","z","b","z"], 673 | ["b","b","b","b","b","b","z","b","z","b","z","b","z","b","z","b","z","b","z","b","z","b","z","b","z","b","z","b","z","b"], 674 | ["b","b","b","b","b","b","b","z","b","z","b","z","b","z","b","z","b","z","b","z","b","z","b","z","b","z","b","z","b","z"], 675 | ["b","b","b","b","b","b","z","b","z","b","z","b","z","b","z","b","z","b","z","b","z","b","z","b","z","b","z","b","z","b"], 676 | ["b","b","b","b","b","b","b","z","b","z","b","z","b","z","b","z","b","z","b","z","b","z","b","z","b","z","b","z","b","z"], 677 | ["b","b","b","b","b","b","z","b","z","b","z","b","z","b","z","b","z","b","z","b","z","b","z","b","z","b","z","b","z","b"], 678 | ["b","b","b","b","b","b","b","z","b","z","b","z","b","z","b","z","b","z","b","z","b","z","b","z","b","z","b","z","b","z"], 679 | ["b","b","b","b","b","b","z","b","z","b","z","b","z","b","z","b","z","b","z","b","z","b","z","b","z","b","z","b","z","b"] 680 | ], 681 | "rotation":[ 682 | [0,0,0,0,0,0,0,2,0,2,0,2,0,2,0,2,0,2,0,2,0,2,0,2,0,2,0,2,0,2], 683 | [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], 684 | [0,0,0,0,0,0,0,2,0,2,0,2,0,2,0,2,0,2,0,2,0,2,0,2,0,2,0,2,0,2], 685 | [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], 686 | [0,0,0,0,0,0,0,2,0,2,0,2,0,2,0,2,0,2,0,2,0,2,0,2,0,2,0,2,0,2], 687 | [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], 688 | [0,0,0,0,0,0,0,2,0,2,0,2,0,2,0,2,0,2,0,2,0,2,0,2,0,2,0,2,0,2], 689 | [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], 690 | [0,0,0,0,0,0,0,2,0,2,0,2,0,2,0,2,0,2,0,2,0,2,0,2,0,2,0,2,0,2], 691 | [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], 692 | [0,0,0,0,0,0,0,2,0,2,0,2,0,2,0,2,0,2,0,2,0,2,0,2,0,2,0,2,0,2], 693 | [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], 694 | [0,0,0,0,0,0,0,2,0,2,0,2,0,2,0,2,0,2,0,2,0,2,0,2,0,2,0,2,0,2], 695 | [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], 696 | [0,0,0,0,0,0,0,2,0,2,0,2,0,2,0,2,0,2,0,2,0,2,0,2,0,2,0,2,0,2], 697 | [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], 698 | [0,0,0,0,0,0,0,2,0,2,0,2,0,2,0,2,0,2,0,2,0,2,0,2,0,2,0,2,0,2], 699 | [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], 700 | [0,0,0,0,0,0,0,2,0,2,0,2,0,2,0,2,0,2,0,2,0,2,0,2,0,2,0,2,0,2], 701 | [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], 702 | [0,0,0,0,0,0,0,2,0,2,0,2,0,2,0,2,0,2,0,2,0,2,0,2,0,2,0,2,0,2], 703 | [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], 704 | [0,0,0,0,0,0,0,2,0,2,0,2,0,2,0,2,0,2,0,2,0,2,0,2,0,2,0,2,0,2], 705 | [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], 706 | [0,0,0,0,0,0,0,2,0,2,0,2,0,2,0,2,0,2,0,2,0,2,0,2,0,2,0,2,0,2], 707 | [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], 708 | [0,0,0,0,0,0,0,2,0,2,0,2,0,2,0,2,0,2,0,2,0,2,0,2,0,2,0,2,0,2], 709 | [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], 710 | [0,0,0,0,0,0,0,2,0,2,0,2,0,2,0,2,0,2,0,2,0,2,0,2,0,2,0,2,0,2], 711 | [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0] 712 | ] 713 | } 714 | ] 715 | `) 716 | --------------------------------------------------------------------------------