├── .gitignore ├── demo.gif ├── thumbnail.png ├── mino ├── README.md ├── LICENSE └── tetris.typ ├── template └── main.typ ├── typst.toml ├── LICENSE ├── README.md └── lib.typ /.gitignore: -------------------------------------------------------------------------------- 1 | test.typ 2 | *.pdf 3 | .DS_Store -------------------------------------------------------------------------------- /demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YouXam/soviet-matrix/HEAD/demo.gif -------------------------------------------------------------------------------- /thumbnail.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YouXam/soviet-matrix/HEAD/thumbnail.png -------------------------------------------------------------------------------- /mino/README.md: -------------------------------------------------------------------------------- 1 | # Mino 2 | 3 | This directory contains a fork of the [Enter-tainer/mino](https://github.com/Enter-tainer/mino) project, with a modification to support custom column counts and stylistic customizations。 4 | 5 | ## License 6 | 7 | [MIT](https://github.com/Enter-tainer/mino/blob/master/LICENSE) -------------------------------------------------------------------------------- /template/main.typ: -------------------------------------------------------------------------------- 1 | #import "@preview/soviet-matrix:0.2.1": game 2 | #show: game.with(seed: 0) 3 | 4 | /* 5 | Move Left: a 6 | Move Right: d 7 | Soft Drop: s 8 | Hard Drop: f 9 | Rotate Left: q 10 | Rotate Right: e 11 | 180-degree Rotate: w 12 | Hold Piece: c 13 | 14 | Enter characters below to get started. 15 | */ 16 | -------------------------------------------------------------------------------- /typst.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "soviet-matrix" 3 | version = "0.2.1" 4 | compiler = "0.13.0" 5 | entrypoint = "lib.typ" 6 | repository = "https://github.com/YouXam/soviet-matrix" 7 | authors = ["YouXam "] 8 | license = "MIT" 9 | description = "Tetris game in Typst" 10 | keywords = ["game", "fun", "tetris"] 11 | categories = ["fun"] 12 | exclude = ["thumbnail.png", "demo.gif"] 13 | 14 | [template] 15 | path = "template" 16 | entrypoint = "main.typ" 17 | thumbnail = "thumbnail.png" 18 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 YouXam 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /mino/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 mgt 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # soviet-matrix 2 | 3 | This is a classic Tetris game implemented using Typst. The goal is to manipulate falling blocks to create and clear horizontal lines without letting the blocks stack up to the top of the playing field. 4 | 5 | ![](./demo.gif) 6 | 7 | ## How to Play 8 | 9 | You can play the game in two ways: 10 | 11 | 1. **Online:** 12 | - Visit [https://typst.app/app?template=soviet-matrix&version=latest](https://typst.app/app?template=soviet-matrix&version=latest). 13 | - Enter any title of your choice and click **Create**. 14 | 15 | 2. **Locally:** 16 | - Open your command line interface. 17 | - Run the following command: 18 | ```bash 19 | typst init @preview/soviet-matrix 20 | ``` 21 | - Typst will create a new directory. 22 | - Open `main.typ` in the created directory. 23 | - Use the [Tinymist Typst VS Code extension](https://marketplace.visualstudio.com/items/?itemName=myriad-dreamin.tinymist) for live preview and gameplay. 24 | 25 | Enjoy the game! 26 | 27 | 28 | ## Controls 29 | 30 | - Move Left: a 31 | - Move Right: d 32 | - Soft Drop: s 33 | - Hard Drop: f 34 | - Rotate Left: q 35 | - Rotate Right: e 36 | - 180-degree Rotate: w 37 | - Hold Piece: c 38 | 39 | ## Changing the Game Seed 40 | 41 | If you want to play different game scenarios, you can change the game seed using the following method: 42 | 43 | ```typst 44 | #import "@preview/soviet-matrix:0.2.1": game 45 | #show: game.with(seed: 123) // Change the game seed 46 | ``` 47 | 48 | Replace `123` with any number of your choice. 49 | 50 | ## Changing Key Bindings 51 | 52 | Modify the `actions` parameter in the `game.with` method to change the key bindings. The default key bindings are as follows: 53 | 54 | 55 | ```typst 56 | #show: game.with(seed: 0, actions: ( 57 | left: ("a", ), 58 | right: ("d", ), 59 | down: ("s", ), 60 | left-rotate: ("q", ), 61 | right-rotate: ("e", ), 62 | half-turn: ("w", ), 63 | fast-drop: ("f", ), 64 | hold-mino: ("c", ), 65 | )) 66 | ``` 67 | 68 | -------------------------------------------------------------------------------- /mino/tetris.typ: -------------------------------------------------------------------------------- 1 | 2 | #let default-color = ( 3 | "Z": rgb("#ef624d"), 4 | "S": rgb("#66c65c"), 5 | "L": rgb("#ef9535"), 6 | "J": rgb("#1983bf"), 7 | "T": rgb("#9c27b0"), 8 | "O": rgb("#f7d33e"), 9 | "I": rgb("#41afde"), 10 | "X": rgb("#686868"), 11 | ) 12 | 13 | #let default-highlight-color = ( 14 | "Z": rgb("#ff9484"), 15 | "S": rgb("#88ee86"), 16 | "L": rgb("#ffbf60"), 17 | "J": rgb("#1ba6f9"), 18 | "T": rgb("#e56add"), 19 | "O": rgb("#fff952"), 20 | "I": rgb("#43d3ff"), 21 | "X": rgb("#949494"), 22 | ) 23 | 24 | #let is-upper(c) = upper(c) == c 25 | 26 | #let process-text(string-field) = { 27 | string-field.trim().split("\n").rev() 28 | } 29 | 30 | #let get-field(field) = { 31 | if type(field) == array { 32 | field 33 | } else if type(field) == str { 34 | process-text(field) 35 | } else if type(field) == content { 36 | process-text(field.text) 37 | } else { 38 | panic("unknown type of field") 39 | } 40 | } 41 | 42 | #let render-field( 43 | field, 44 | rows: 20, 45 | cols: 10, 46 | cell-size: 10pt, 47 | bg-color: rgb("#f3f3ed"), 48 | stroke: none, 49 | radius: auto, 50 | shadow: true, 51 | highlight: true, 52 | color-data: default-color, 53 | highlight-color-data: default-highlight-color, 54 | shadow-color: rgb("#6f6f6f17"), 55 | overdraw: 5 56 | ) = { 57 | let field = get-field(field) 58 | let actual-radius = if radius == auto { 59 | cell-size / 4 60 | } else { 61 | radius 62 | } 63 | let highlight-height = cell-size / 5 64 | let shadow-offset-vertical = cell-size * 0.4 65 | let shadow-offset-horizontal = cell-size / 4 66 | 67 | block( 68 | width: cols * cell-size + highlight-height, 69 | height: rows * cell-size + highlight-height, 70 | inset: 0pt, 71 | stroke: stroke, 72 | radius: actual-radius, 73 | clip: true, 74 | fill: bg-color, 75 | breakable: false, 76 | { 77 | let max-row = calc.min(rows, field.len()) 78 | if shadow { 79 | for i in range(max-row) { 80 | let cells = field.at(i).len() 81 | let loop-max = calc.min(cols, cells) 82 | for j in range(loop-max) { 83 | if field.at(i).codepoints().at(j) == "_" { 84 | continue 85 | } 86 | let block = field.at(i).codepoints().at(j) 87 | if is-upper(block) { 88 | place( 89 | top + left, 90 | dx: cell-size * j + shadow-offset-horizontal, 91 | dy: cell-size * (rows - 1 - i) + shadow-offset-vertical, 92 | rect( 93 | width: cell-size, 94 | height: cell-size, 95 | fill: shadow-color, 96 | ), 97 | ) 98 | } 99 | } 100 | } 101 | } 102 | for i in range(max-row) { 103 | let cells = field.at(i).len() 104 | let loop-max = calc.min(cols, cells) 105 | for j in range(loop-max) { 106 | if field.at(i).codepoints().at(j) == "_" { 107 | continue 108 | } 109 | let block = field.at(i).codepoints().at(j) 110 | for _ in range(overdraw) { 111 | if is-upper(block) { 112 | place( 113 | top + left, 114 | dx: cell-size * j, 115 | dy: cell-size * (rows - 1 - i) + highlight-height, 116 | rect( 117 | width: cell-size, 118 | height: cell-size, 119 | fill: color-data.at(upper(block)), 120 | ), 121 | ) 122 | if highlight and block != "_" { 123 | let (sx, sy) = (0pt, 0pt) 124 | place( 125 | top + left, 126 | dx: cell-size * j, 127 | dy: cell-size * (rows - i - 1), 128 | polygon( 129 | fill: highlight-color-data.at(upper(block)), 130 | (highlight-height, 0pt), 131 | (0pt, highlight-height), 132 | (cell-size, highlight-height), 133 | (cell-size + highlight-height, 0pt), 134 | ), 135 | ) 136 | 137 | place( 138 | top + left, 139 | dx: cell-size * j + cell-size, 140 | dy: cell-size * (rows - i - 1), 141 | polygon( 142 | fill: highlight-color-data.at(upper(block)).lighten(-10%), 143 | (highlight-height, 0pt), 144 | (0pt, highlight-height), 145 | (0pt, highlight-height + cell-size), 146 | (highlight-height, cell-size), 147 | ), 148 | ) 149 | } 150 | } else { 151 | // operation mino is displayed in lower case 152 | place( 153 | top + left, 154 | dx: cell-size * j, 155 | dy: cell-size * (rows - 1 - i) + highlight-height, 156 | rect( 157 | width: cell-size, 158 | height: cell-size, 159 | fill: bg-color, 160 | ), 161 | ) 162 | place( 163 | top + left, 164 | dx: cell-size * j + highlight-height, 165 | dy: cell-size * (rows - 1 - i), 166 | rect( 167 | width: cell-size, 168 | height: cell-size, 169 | inset: 0pt, 170 | stroke: 0pt, 171 | align(center + horizon)[ 172 | #rect( 173 | width: cell-size * 0.8, 174 | height: cell-size * 0.8, 175 | fill: color-data.at(upper(block)).lighten(70%), 176 | stroke: color-data.at(upper(block)) + cell-size * 0.05, 177 | ) 178 | ] 179 | ), 180 | ) 181 | } 182 | } 183 | } 184 | } 185 | }, 186 | ) 187 | } -------------------------------------------------------------------------------- /lib.typ: -------------------------------------------------------------------------------- 1 | #import "@preview/suiji:0.4.0": gen-rng, choice, shuffle 2 | #import "mino/tetris.typ": render-field 3 | 4 | #let parse-actions(body) = { 5 | let extract(it) = { 6 | "" 7 | if it == [ ] { 8 | " " 9 | } else if it.func() == text { 10 | it.text 11 | } else if it.func() == [].func() { 12 | it.children.map(extract).join() 13 | } 14 | } 15 | extract(body).clusters().map(lower) 16 | } 17 | 18 | #let minoes = (("ZZ_", "_ZZ"), ("OO", "OO"), ("_SS", "SS_"), ("IIII",), ("__L", "LLL"), ("J__", "JJJ"), ("_T_", "TTT")) 19 | 20 | #let shuffle-minoes(rng) = { 21 | let indices = range(minoes.len()) 22 | let (rng-after-shuffle, shuffled) = shuffle(rng, indices) 23 | (rng-after-shuffle, shuffled.map(i => minoes.at(i))) 24 | } 25 | 26 | #let create-mino(mino, cols, rows) = { 27 | let width = calc.max(..mino.map(it => it.len())) 28 | ( 29 | mino: mino, 30 | pos: (x: calc.floor(cols / 2) - calc.floor(width / 2) - 1, y: rows + 1), 31 | height: mino.len(), 32 | width: width, 33 | index: minoes.position((value)=>{value==mino}) 34 | ) 35 | } 36 | 37 | #let new-mino(rng, bag, cols, rows) = { 38 | let mino-bag = bag 39 | let rng-before-draw = rng 40 | let mino = none 41 | let bag-before-draw = mino-bag 42 | let rng-after-bag = rng-before-draw 43 | let bag-after-bag = bag-before-draw 44 | if mino-bag.len() == 0 { 45 | let (rng-after-shuffle, new-bag) = shuffle-minoes(rng-before-draw) 46 | bag-after-bag = new-bag 47 | rng-after-bag = rng-after-shuffle 48 | } 49 | mino = bag-after-bag.at(0) 50 | let bag-after-draw = bag-after-bag.slice(1, bag-after-bag.len()) 51 | (rng-after-bag, bag-after-draw, create-mino(mino, cols, rows)) 52 | } 53 | 54 | #let render-map(map, bg-color: rgb("#f3f3ed")) = { 55 | let map = map.map(it => it.join("")) 56 | render-field(map, rows: map.len(), cols: calc.max(..map.map(it => it.len())), bg-color: bg-color, radius: 0pt) 57 | } 58 | 59 | #let check-collision(state, cols: 10, rows: 20) = { 60 | if state.current.pos.x < 0 or state.current.pos.y - state.current.height + 1 < 0 or state.current.pos.x + state.current.width > cols { 61 | return true 62 | } 63 | 64 | for y in range(state.current.mino.len()) { 65 | for x in range(state.current.mino.at(y).len()) { 66 | if state.current.mino.at(y).at(x) != "_" and state.map.at(state.current.pos.y - y).at(state.current.pos.x + x) != "_" { 67 | return true 68 | } 69 | } 70 | } 71 | false 72 | } 73 | 74 | #let try-move(state, cols: 10, rows: 20, dx: 0, dy: 0) = { 75 | state.current.pos = (x: state.current.pos.x + dx, y: state.current.pos.y + dy) 76 | not check-collision(state, cols: cols, rows: rows) 77 | } 78 | 79 | #let rotate-clockwise(mino) = { 80 | let center = (x: mino.pos.x + calc.floor(mino.width / 2), y: mino.pos.y - calc.floor(mino.height / 2)) 81 | 82 | let new-mino = range(mino.width).map(_ => "") 83 | for y in range(mino.mino.len() - 1, -1, step: -1) { 84 | for x in range(mino.mino.at(y).len()) { 85 | new-mino.at(x) += mino.mino.at(y).at(x) 86 | } 87 | } 88 | 89 | ( 90 | mino: new-mino, 91 | pos: (x: center.x - calc.floor(mino.height / 2), y: center.y + calc.floor(mino.width / 2)), 92 | height: mino.width, 93 | width: mino.height, 94 | index: mino.index, 95 | ) 96 | } 97 | 98 | #let rotate(state, cols: 10, rows: 20, angle: 0) = { 99 | let next-state = state 100 | for _ in range(angle) { 101 | next-state.current = rotate-clockwise(next-state.current) 102 | } 103 | if check-collision(next-state, cols: cols, rows: rows) { 104 | state 105 | } else { 106 | next-state 107 | } 108 | } 109 | 110 | #let move(state, cols: 10, rows: 20, dx: 0, dy: 0) = { 111 | if try-move(state, cols: cols, rows: rows, dx: dx, dy: dy) { 112 | state.current.pos.x += dx 113 | state.current.pos.y += dy 114 | (state, true) 115 | } else { 116 | (state, false) 117 | } 118 | } 119 | 120 | #let hold(state, cols: 10, rows: 20) = { 121 | if state.can-hold == false { 122 | return state 123 | } 124 | state.can-hold = false 125 | if state.hold == none { 126 | state.hold = create-mino(minoes.at(state.current.index), cols, rows) 127 | state.current = state.next 128 | (state.rng, state.mino-bag, state.next) = new-mino(state.rng, state.mino-bag, cols, rows) 129 | } else { 130 | let hold-mino = state.hold 131 | state.hold = create-mino(minoes.at(state.current.index), cols, rows) 132 | state.current = hold-mino 133 | } 134 | return state 135 | } 136 | 137 | #let render(state, cols: 10, rows: 20) = { 138 | let map = state.map 139 | 140 | if not state.end { 141 | let pos = state.current.pos 142 | 143 | while true { 144 | if try-move(state, cols: cols, rows: rows, dy: -1) { 145 | state.current.pos.y -= 1 146 | } else { 147 | break 148 | } 149 | } 150 | 151 | for y in range(state.current.mino.len()) { 152 | for x in range(state.current.mino.at(y).len()) { 153 | if state.current.mino.at(y).at(x) != "_" { 154 | map.at(state.current.pos.y - y).at(state.current.pos.x + x) = lower(state.current.mino.at(y).at(x)) 155 | } 156 | } 157 | } 158 | 159 | for y in range(state.current.mino.len()) { 160 | for x in range(state.current.mino.at(y).len()) { 161 | if state.current.mino.at(y).at(x) != "_" { 162 | map.at(pos.y - y).at(pos.x + x) = state.current.mino.at(y).at(x) 163 | } 164 | } 165 | } 166 | } 167 | 168 | let main = state.map.slice(0, state.map.len() - 2) 169 | 170 | grid(columns: 2, gutter: 5pt, block(height: rows * 10pt, width: cols * 10pt, { 171 | place( 172 | top + left, 173 | dy: 40pt, 174 | dx: 2pt, 175 | block(stroke: luma(80%) + 0.5pt, radius: 2pt, inset: 0pt, fill: tiling(size: (10pt, 10pt))[ 176 | #box(stroke: 0.1pt + luma(50%), width: 100%, height: 100%, fill: rgb("#f3f3ed")), 177 | ], height: rows * 10pt, width: cols * 10pt), 178 | ) 179 | place(top + left, render-map(map, bg-color: white.transparentize(100%))) 180 | }), pad(top: 40pt, [ 181 | #set block(spacing: 3pt) 182 | #block(height: 6em, width: 6em, stroke: luma(80%) + 0.5pt, radius: 2pt, [ 183 | #set block(spacing: 0pt) 184 | #if state.end [ 185 | #align(center + horizon)[ 186 | *Game Over* 187 | ] 188 | ] else [ 189 | #pad(top: 2pt, left: 3pt, bottom: 0pt, [*Next*]) 190 | #align(center + horizon)[ 191 | #render-map(state.next.mino.map(it => it.split("")).rev(), bg-color: white.transparentize(100%)) 192 | ] 193 | ] 194 | ]) 195 | #block(height: 4em, width: 6em, stroke: luma(80%) + 0.5pt, radius: 2pt, [ 196 | #set block(spacing: 0pt) 197 | #pad(top: 2pt, left: 3pt, bottom: 0pt, [*Hold*]) 198 | #if state.hold != none [ 199 | #align(center + horizon)[ 200 | #render-map(state.hold.mino.map(it => it.split("")).rev(), bg-color: white.transparentize(100%)) 201 | ] 202 | ] else [ 203 | #align(center + horizon)[ 204 | None 205 | ] 206 | ] 207 | ]) 208 | #block(height: 4em, width: 6em, stroke: luma(80%) + 0.5pt, radius: 2pt, [ 209 | #set block(spacing: 0pt) 210 | #pad(top: 2pt, left: 3pt, bottom: 0pt, [*Score*]) 211 | #align(center + horizon)[ 212 | #state.score 213 | ] 214 | ]) 215 | ])) 216 | } 217 | 218 | #let next-tick(state, cols: 10, rows: 10) = { 219 | if try-move(state, dy: -1, cols: cols, rows: rows) { 220 | state.current.pos.y -= 1 221 | } else { 222 | for y in range(state.current.mino.len()) { 223 | for x in range(state.current.mino.at(y).len()) { 224 | if state.current.mino.at(y).at(x) != "_" { 225 | state.map.at(state.current.pos.y - y).at(state.current.pos.x + x) = state.current.mino.at(y).at(x) 226 | } 227 | } 228 | } 229 | state.can-hold = true 230 | state.current = state.next 231 | (state.rng, state.mino-bag, state.next) = new-mino(state.rng, state.mino-bag, cols, rows) 232 | } 233 | state 234 | } 235 | 236 | #let eliminate(state, cols: 10, rows: 20) = { 237 | let new-map = state.map.filter(row => row.filter(it => it != "_").len() != row.len()) 238 | let eliminated = state.map.len() - new-map.len() 239 | state.map = new-map + range(eliminated).map(_ => range(cols).map(it => "_")) 240 | let level = (-2, 40, 100, 300, 1200) 241 | state.score += level.at(eliminated) 242 | state 243 | } 244 | 245 | #let game(body, seed: 2, cols: 10, rows: 20, actions: ( 246 | left: ("a", ), 247 | right: ("d", ), 248 | down: ("s", ), 249 | left-rotate: ("q", ), 250 | right-rotate: ("e", ), 251 | half-turn: ("w", ), 252 | fast-drop: ("f", ), 253 | hold-mino: ("c", ), 254 | )) = { 255 | set page(height: auto, width: auto, margin: (top: 0.5in - 30pt, bottom: 0.5in + 40pt, rest: 0.5in)) 256 | 257 | let chars = parse-actions(body) 258 | 259 | let rng-initial = gen-rng(seed) 260 | let (rng-after-initial-bag, bag-after-initial-bag) = shuffle-minoes(rng-initial) 261 | let state = ( 262 | rng: rng-after-initial-bag, 263 | mino-bag: bag-after-initial-bag, 264 | current: none, 265 | next: none, 266 | map: range(rows + 4).map(_ => range(cols).map(it => "_")), 267 | end: false, 268 | score: 0, 269 | hold: none, 270 | can-hold: true, 271 | ) 272 | 273 | (state.rng, state.mino-bag, state.current) = new-mino(rng-after-initial-bag, bag-after-initial-bag, cols, rows) 274 | (state.rng, state.mino-bag, state.next) = new-mino(state.rng, state.mino-bag, cols, rows) 275 | 276 | for char in chars { 277 | if actions.left.any(it => it == char) { 278 | (state, _) = move(state, cols: cols, rows: rows, dx: -1, dy: 0) 279 | } else if actions.right.any(it => it == char) { 280 | (state, _) = move(state, cols: cols, rows: rows, dx: 1, dy: 0) 281 | } else if actions.fast-drop.any(it => it == char) { 282 | let success = true 283 | while success { 284 | (state, success) = move(state, cols: cols, rows: rows, dy: -1) 285 | } 286 | } else if actions.right-rotate.any(it => it == char) { 287 | state = rotate(state, angle: 1, cols: cols, rows: rows) 288 | } else if actions.left-rotate.any(it => it == char) { 289 | state = rotate(state, angle: 3, cols: cols, rows: rows) 290 | } else if actions.half-turn.any(it => it == char) { 291 | state = rotate(state, angle: 2, cols: cols, rows: rows) 292 | } else if actions.hold-mino.any(it => it == char) { 293 | state = hold(state, cols: cols, rows: rows) 294 | } 295 | state = next-tick(state, cols: cols, rows: rows) 296 | state = eliminate(state, cols: cols, rows: rows) 297 | 298 | if check-collision(state, cols: cols, rows: rows) { 299 | state.end = true 300 | break 301 | } 302 | } 303 | 304 | render(state, cols: cols, rows: rows) 305 | } 306 | --------------------------------------------------------------------------------