├── .python-version ├── data ├── version.txt ├── pico8_version.txt └── cartridges.txt ├── HH.png ├── README.md ├── XX_I.png ├── cnots.png ├── logo.png ├── title.gif ├── controls.png ├── favicon.ico ├── icon-16.png ├── icon-192.png ├── icon-32.png ├── icon-512.png ├── win.aseprite ├── background.png ├── logo.aseprite ├── lose.aseprite ├── qpu_vs_qpu.gif ├── .gitattributes ├── .projectile ├── sprites.aseprite ├── apple-touch-icon.png ├── .gitmodules ├── .gitignore ├── manifest.webmanifest ├── test ├── board │ ├── render_utest.lua │ ├── board_is_empty_utest.lua │ └── swap_utest.lua ├── player_utest.lua ├── class_self_utest.lua ├── test_helper.lua ├── subtle_issues_with_cnot_and_swap_utest.lua ├── performance_utest.lua ├── block │ ├── h_block_utest.lua │ ├── i_block_utest.lua │ ├── q_block_utest.lua │ ├── s_block_utest.lua │ ├── t_block_utest.lua │ ├── x_block_utest.lua │ ├── y_block_utest.lua │ ├── z_block_utest.lua │ ├── hash_block_utest.lua │ ├── swap_block_utest.lua │ ├── cnot_x_block_utest.lua │ ├── control_block_utest.lua │ ├── garbage_block_utest.lua │ └── hover_utest.lua ├── particle_utest.lua ├── sash_utest.lua ├── chain_callback_utest.lua ├── cursor_utest.lua ├── combo_callback_utest.lua ├── block_utest.lua └── block_fall_utest.lua ├── src ├── lib │ ├── player.lua │ ├── high_score.lua │ ├── cursor.lua │ ├── helpers.lua │ ├── replay.lua │ ├── pending_garbage_blocks.lua │ ├── reduction_rules.lua │ ├── effects.lua │ ├── qpu.lua │ ├── block.lua │ └── game.lua ├── title │ ├── plasma.lua │ ├── menu.lua │ └── game.lua ├── main_vs_qpu.lua ├── main_vs_human.lua ├── main_qpu_vs_qpu.lua ├── tutorial │ ├── ion.lua │ ├── game.lua │ └── dtb.lua ├── main_endless.lua ├── main_rush.lua ├── main_title.lua ├── main_tutorial.lua └── rush │ └── game.lua ├── LICENSE ├── scripts ├── test.sh ├── build_and_install_all_cartridges.sh ├── install_all_cartridges.sh ├── build_all_cartridges.sh ├── install_single_cartridge.sh ├── install_data_cartridges_with_merging.sh ├── build_single_cartridge.sh ├── export_game_release.p8 └── export_and_patch_cartridge_release.sh ├── .vscode └── settings.json ├── Rakefile └── icon.svg /.python-version: -------------------------------------------------------------------------------- 1 | 3.6.15 2 | -------------------------------------------------------------------------------- /data/version.txt: -------------------------------------------------------------------------------- 1 | 0.7.5 2 | -------------------------------------------------------------------------------- /data/pico8_version.txt: -------------------------------------------------------------------------------- 1 | 0.2.5c 2 | -------------------------------------------------------------------------------- /HH.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qniapp/quantattack/HEAD/HH.png -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # テストの実行 2 | 3 | ``` 4 | % rake test 5 | ``` 6 | 7 | -------------------------------------------------------------------------------- /XX_I.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qniapp/quantattack/HEAD/XX_I.png -------------------------------------------------------------------------------- /cnots.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qniapp/quantattack/HEAD/cnots.png -------------------------------------------------------------------------------- /logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qniapp/quantattack/HEAD/logo.png -------------------------------------------------------------------------------- /title.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qniapp/quantattack/HEAD/title.gif -------------------------------------------------------------------------------- /controls.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qniapp/quantattack/HEAD/controls.png -------------------------------------------------------------------------------- /favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qniapp/quantattack/HEAD/favicon.ico -------------------------------------------------------------------------------- /icon-16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qniapp/quantattack/HEAD/icon-16.png -------------------------------------------------------------------------------- /icon-192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qniapp/quantattack/HEAD/icon-192.png -------------------------------------------------------------------------------- /icon-32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qniapp/quantattack/HEAD/icon-32.png -------------------------------------------------------------------------------- /icon-512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qniapp/quantattack/HEAD/icon-512.png -------------------------------------------------------------------------------- /win.aseprite: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qniapp/quantattack/HEAD/win.aseprite -------------------------------------------------------------------------------- /background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qniapp/quantattack/HEAD/background.png -------------------------------------------------------------------------------- /logo.aseprite: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qniapp/quantattack/HEAD/logo.aseprite -------------------------------------------------------------------------------- /lose.aseprite: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qniapp/quantattack/HEAD/lose.aseprite -------------------------------------------------------------------------------- /qpu_vs_qpu.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qniapp/quantattack/HEAD/qpu_vs_qpu.gif -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /.projectile: -------------------------------------------------------------------------------- 1 | -backup 2 | -build 3 | -intermediate 4 | -pico-boots 5 | -profiler.log 6 | -------------------------------------------------------------------------------- /sprites.aseprite: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qniapp/quantattack/HEAD/sprites.aseprite -------------------------------------------------------------------------------- /apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qniapp/quantattack/HEAD/apple-touch-icon.png -------------------------------------------------------------------------------- /data/cartridges.txt: -------------------------------------------------------------------------------- 1 | title 2 | tutorial 3 | rush 4 | endless 5 | vs_qpu 6 | qpu_vs_qpu 7 | vs_human 8 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "pico-boots"] 2 | path = pico-boots 3 | url = https://github.com/hsandt/pico-boots.git 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.p8l 2 | .DS_Store 3 | activity_log.txt 4 | backup 5 | build/ 6 | config.txt 7 | doc/ 8 | intermediate/ 9 | log.txt 10 | profiler.log 11 | sdl_controllers.txt 12 | -------------------------------------------------------------------------------- /manifest.webmanifest: -------------------------------------------------------------------------------- 1 | { 2 | "name": "QuantAttack", 3 | "icons": [ 4 | { "src": "./icon-192.png", "type": "image/png", "sizes": "192x192" }, 5 | { "src": "./icon-512.png", "type": "image/png", "sizes": "512x512" } 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /test/board/render_utest.lua: -------------------------------------------------------------------------------- 1 | require("engine/test/bustedhelper") 2 | require("test/test_helper") 3 | require("lib/board") 4 | 5 | describe('board', function() 6 | local board 7 | 8 | before_each(function() 9 | board = board_class() 10 | end) 11 | 12 | describe('render', function() 13 | it("空の盤面を表示する", function() 14 | assert.has_no_errors(function() board:render() end) 15 | end) 16 | end) 17 | end) 18 | -------------------------------------------------------------------------------- /src/lib/player.lua: -------------------------------------------------------------------------------- 1 | --- プレーヤー (人間) 2 | player_class = new_class() 3 | 4 | function player_class._init(_ENV, _number) 5 | number = _number or 0 6 | init(_ENV) 7 | end 8 | 9 | --- 初期化 10 | function player_class.init(_ENV) 11 | score = 0 12 | end 13 | 14 | --- プレーヤーの入力を更新 15 | function player_class.update(_ENV) 16 | left, right, up, down, x, o = btnp(0, number), btnp(1, number), btnp(2, number), btnp(3, number), btnp(5, number), btn(4, number) 17 | end 18 | -------------------------------------------------------------------------------- /src/title/plasma.lua: -------------------------------------------------------------------------------- 1 | local time, colors = 0, split("0,5,12,5,12") 2 | 3 | function render_plasma() 4 | local t = time / 12 5 | local sint, cost = sin(t / 15) / 120, cos(t / 17) / 180 6 | 7 | for x = 0, 31 do 8 | for y = 0, 31 do 9 | local v = sin(((x * sint) + y * cost) * 8 + t) 10 | v = flr((v * cos(x / 53 + y / 57) + 1) * 2.5) 11 | pset(x << 2, y << 2, colors[v + 1]) 12 | end 13 | end 14 | 15 | time = time + 0.05 16 | end 17 | -------------------------------------------------------------------------------- /src/lib/high_score.lua: -------------------------------------------------------------------------------- 1 | ---@diagnostic disable: undefined-global 2 | 3 | cartdata("quantattack_0_7_5") 4 | 5 | high_score_class = new_class() 6 | 7 | function high_score_class:_init(id) 8 | self.id = id 9 | end 10 | 11 | function high_score_class:get() 12 | return dget(self.id) or 0 13 | end 14 | 15 | function high_score_class:put(score) 16 | if self:get() < score then 17 | dset(self.id, score) 18 | return true 19 | end 20 | 21 | return false 22 | end 23 | -------------------------------------------------------------------------------- /test/player_utest.lua: -------------------------------------------------------------------------------- 1 | require("engine/test/bustedhelper") 2 | require("lib/player") 3 | 4 | describe('player', function() 5 | local player 6 | 7 | before_each(function() 8 | player = player_class() 9 | end) 10 | 11 | describe('constructor', function() 12 | it("creates a player with score = 0", function() 13 | assert.are.equal(0, player.score) 14 | end) 15 | end) 16 | 17 | describe('init', function() 18 | it("resets score", function() 19 | player.score = 1 >> 16 20 | 21 | player:init() 22 | 23 | assert.are.equal(0, player.score) 24 | end) 25 | end) 26 | end) 27 | -------------------------------------------------------------------------------- /test/class_self_utest.lua: -------------------------------------------------------------------------------- 1 | require("engine/test/bustedhelper") 2 | require("lib/helpers") 3 | 4 | global_variable = "global_variable" 5 | local foo_class = new_class() 6 | 7 | function foo_class:_init() 8 | self.state = "idle" 9 | end 10 | 11 | -- self を省略したいときの書きかた 12 | function foo_class.access_instance_variable(_ENV) 13 | local foo = "state = " .. state 14 | end 15 | 16 | -- こちらは失敗 17 | function foo_class.access_global_variable(_ENV) 18 | printh("global_variable = " .. global_variable) 19 | end 20 | 21 | describe('self を省略できる', function() 22 | it('呼び出された側で self. を省略できる', function() 23 | local foo = foo_class() 24 | assert.has_no.errors(function() foo:access_instance_variable() end) 25 | assert.has.errors(function() foo:access_global_variable() end) 26 | end) 27 | end) 28 | -------------------------------------------------------------------------------- /test/test_helper.lua: -------------------------------------------------------------------------------- 1 | require("lib/helpers") 2 | require("lib/board") 3 | 4 | function dump(o) 5 | if type(o) == 'table' then 6 | local s = '{ ' 7 | for k,v in pairs(o) do 8 | if type(k) ~= 'number' then k = '"'..k..'"' end 9 | s = s .. '['..k..'] = ' .. dump(v) .. ',' 10 | end 11 | return s .. '} ' 12 | else 13 | return tostring(o) 14 | end 15 | end 16 | 17 | function wait_swap_to_finish(board) 18 | for _ = 1, block_class.block_swap_animation_frame_count + 1 do 19 | board:update() 20 | end 21 | end 22 | 23 | function control_block(other_x) 24 | local control = block_class("control") 25 | control.other_x = other_x 26 | return control 27 | end 28 | 29 | function cnot_x_block(other_x) 30 | local cnot_x = block_class("cnot_x") 31 | cnot_x.other_x = other_x 32 | return cnot_x 33 | end 34 | 35 | function swap_block(other_x) 36 | local swap = block_class("swap") 37 | swap.other_x = other_x 38 | return swap 39 | end 40 | -------------------------------------------------------------------------------- /test/subtle_issues_with_cnot_and_swap_utest.lua: -------------------------------------------------------------------------------- 1 | require("engine/test/bustedhelper") 2 | require("engine/render/color") 3 | require("test/test_helper") 4 | require("lib/effects") 5 | require("lib/board") 6 | 7 | 8 | describe('CNOT や SWAP が残る/ちぎれる問題', function() 9 | local board 10 | 11 | before_each(function() 12 | board = board_class() 13 | end) 14 | 15 | it("マッチ中のブロックは落下しない", function() 16 | board:put(3, 3, cnot_x_block(5)) 17 | board:put(5, 3, control_block(3)) 18 | board:put(3, 2, cnot_x_block(4)) 19 | board:put(4, 2, control_block(3)) 20 | board:put(1, 1, cnot_x_block(4)) 21 | board:put(4, 1, control_block(1)) 22 | 23 | board:swap(4, 3) 24 | 25 | board:update() 26 | 27 | board:swap(3, 1) 28 | 29 | for _i = 0, 67 do 30 | board:update() 31 | end 32 | 33 | board:swap(2, 1) 34 | 35 | for _i = 61, 73 do 36 | board:update() 37 | end 38 | 39 | assert.are.equal("i", board:block_at(3, 1).type) 40 | assert.are.equal("i", board:block_at(4, 1).type) 41 | end) 42 | end) 43 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 TIS Inc. 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. 22 | -------------------------------------------------------------------------------- /src/lib/cursor.lua: -------------------------------------------------------------------------------- 1 | ---@diagnostic disable: lowercase-global 2 | 3 | --- ユーザまたは QPU が操作するカーソルのクラス 4 | cursor_class = new_class() 5 | 6 | function cursor_class._init(_ENV) 7 | init(_ENV) 8 | end 9 | 10 | --- カーソルの初期化 11 | function cursor_class.init(_ENV) 12 | x, y, _tick = 3, 6, 0 13 | end 14 | 15 | --- カーソルを左に移動 16 | function cursor_class.move_left(_ENV) 17 | if x > 1 then 18 | x = x - 1 19 | end 20 | end 21 | 22 | --- カーソルを右に移動 23 | function cursor_class.move_right(_ENV, cols) 24 | if x < cols - 1 then 25 | x = x + 1 26 | end 27 | end 28 | 29 | --- カーソルを上に移動 30 | function cursor_class.move_up(_ENV, rows) 31 | if y < rows then 32 | y = y + 1 33 | end 34 | end 35 | 36 | --- カーソルを下に移動 37 | function cursor_class.move_down(_ENV) 38 | if y > 1 then 39 | y = y - 1 40 | end 41 | end 42 | 43 | --- カーソルの状態を更新 44 | function cursor_class.update(_ENV) 45 | _tick = (_tick + 1) % 28 46 | end 47 | 48 | --- カーソルを描画 49 | function cursor_class.render(_ENV, screen_x, screen_y) 50 | if _tick < 14 then 51 | sspr(32, 32, 19, 11, screen_x - 2, screen_y - 2) 52 | else 53 | sspr(56, 32, 21, 13, screen_x - 3, screen_y - 3) 54 | end 55 | end 56 | -------------------------------------------------------------------------------- /src/main_vs_qpu.lua: -------------------------------------------------------------------------------- 1 | require("lib/helpers") 2 | require("lib/effects") 3 | require("lib/board") 4 | require("lib/player") 5 | require("lib/qpu") 6 | require("lib/game") 7 | 8 | local game, board, qpu_board = 9 | game_class(), board_class(cursor_class(), 3), board_class(cursor_class(), 78) 10 | 11 | board.block_offset_target, qpu_board.block_offset_target, board.attack_ion_target, qpu_board.attack_ion_target = 12 | { 27, 9 }, { 102, 9 }, { 102, 9 }, { 27, 9 } 13 | 14 | game:add_player(player_class(), board, qpu_board) 15 | game:add_player(qpu_class(qpu_board, stat(6)), qpu_board, board) 16 | 17 | function _init() 18 | game:init() 19 | end 20 | 21 | function _update60() 22 | game:update() 23 | 24 | if game:is_game_over() and t() - game.game_over_time > 2 then 25 | board.show_gameover_menu = true 26 | if btnp(5) then -- x でリプレイ 27 | sfx(15) 28 | _init() 29 | elseif btnp(4) then -- c でタイトルへ戻る 30 | jump('quantattack_title') 31 | end 32 | end 33 | end 34 | 35 | function _draw() 36 | cls() 37 | 38 | game:render() 39 | 40 | print_outlined(unpack_split("time,57,106,7,0")) 41 | print_outlined(game:elapsed_time_string(), unpack_split("55,114,7,0")) 42 | end 43 | -------------------------------------------------------------------------------- /src/main_vs_human.lua: -------------------------------------------------------------------------------- 1 | require("lib/helpers") 2 | require("lib/effects") 3 | require("lib/board") 4 | require("lib/player") 5 | require("lib/game") 6 | 7 | local game, board0, board1 = 8 | game_class(), board_class(cursor_class(), 3), board_class(cursor_class(), 78) 9 | 10 | board0.block_offset_target, board1.block_offset_target, board0.attack_ion_target, board1.attack_ion_target = 11 | { 27, 9 }, { 102, 9 }, { 102, 9 }, { 27, 9 } 12 | 13 | game:add_player(player_class(0), board0, board1) 14 | game:add_player(player_class(1), board1, board0) 15 | 16 | function _init() 17 | game:init() 18 | end 19 | 20 | function _update60() 21 | game:update() 22 | 23 | if game:is_game_over() and t() - game.game_over_time > 2 then 24 | board0.show_gameover_menu = true 25 | board1.show_gameover_menu = true 26 | if btnp(5, 0) or btnp(5, 1) then -- x でリプレイ 27 | sfx(15) 28 | _init() 29 | elseif btnp(4, 0) or btnp(4, 1) then -- c でタイトルへ戻る 30 | jump('quantattack_title') 31 | end 32 | end 33 | end 34 | 35 | function _draw() 36 | cls() 37 | 38 | game:render() 39 | 40 | print_outlined(unpack_split("time,57,106,7,0")) 41 | print_outlined(game:elapsed_time_string(), unpack_split("55,114,7,0")) 42 | end 43 | -------------------------------------------------------------------------------- /test/performance_utest.lua: -------------------------------------------------------------------------------- 1 | require("engine/test/bustedhelper") 2 | require("lib/helpers") 3 | require("lib/effects") 4 | require("lib/board") 5 | require("lib/qpu") 6 | require("lib/game") 7 | 8 | local profiler = require("test/profiler") 9 | 10 | describe('パフォーマンス', function() 11 | it("QPU vs QPU のプロファイルを取る", function() 12 | local qpu1_cursor = cursor_class() 13 | local qpu2_cursor = cursor_class() 14 | local qpu1_board = board_class(qpu1_cursor) 15 | local qpu2_board = board_class(qpu2_cursor) 16 | local qpu1 = qpu_class(qpu1_board) 17 | local qpu2 = qpu_class(qpu2_board) 18 | local game = game_class() 19 | 20 | qpu1_board:put_random_blocks() 21 | qpu1_board.block_offset_target = { 48, 15 } 22 | qpu1_board.attack_ion_target = { 78, 15 } 23 | qpu1_cursor:init() 24 | 25 | qpu2_board:put_random_blocks() 26 | qpu2_board.block_offset_target = { 78, 15 } 27 | qpu2_board.attack_ion_target = { 48, 15 } 28 | qpu2_cursor:init() 29 | 30 | game:init() 31 | game:add_player(qpu1, qpu1_board, qpu2_board) 32 | game:add_player(qpu2, qpu2_board, qpu1_board) 33 | 34 | profiler.start() 35 | for i = 1, 4000 do 36 | game:update() 37 | end 38 | profiler.stop() 39 | 40 | profiler.report("profiler.log") 41 | end) 42 | end) 43 | -------------------------------------------------------------------------------- /test/block/h_block_utest.lua: -------------------------------------------------------------------------------- 1 | require("engine/test/bustedhelper") 2 | require("lib/board") 3 | 4 | describe('h_block', function() 5 | local h 6 | 7 | before_each(function() 8 | h = block_class("h") 9 | end) 10 | 11 | describe(".type", function() 12 | it('should be "h"', function() 13 | assert.are.equals("h", h.type) 14 | end) 15 | end) 16 | 17 | describe(".state", function() 18 | it('should be "idle"', function() 19 | assert.is_true(h.state == "idle") 20 | end) 21 | end) 22 | 23 | describe(".span", function () 24 | it("should be 1", function() 25 | assert.are.equals(1, h.span) 26 | end) 27 | end) 28 | 29 | describe(".height", function () 30 | it("should be 1", function() 31 | assert.are.equals(1, h.height) 32 | end) 33 | end) 34 | 35 | describe("stringify", function() 36 | it('should return "H"', function() 37 | assert.are.equals("H ", stringify(h)) 38 | end) 39 | end) 40 | 41 | describe("is_not_fallable()", function() 42 | it("should return false", function() 43 | assert.is_false(h:is_not_fallable()) 44 | end) 45 | end) 46 | 47 | describe("is_reducible()", function() 48 | it("should return true", function() 49 | assert.is_true(h:is_reducible()) 50 | end) 51 | end) 52 | end) 53 | -------------------------------------------------------------------------------- /test/block/i_block_utest.lua: -------------------------------------------------------------------------------- 1 | require("engine/test/bustedhelper") 2 | require('lib/block') 3 | 4 | describe('i_block', function() 5 | local i 6 | 7 | before_each(function() 8 | i = block_class("i") 9 | end) 10 | 11 | describe(".type", function() 12 | it('should be "i"', function() 13 | assert.are.equals("i", i.type) 14 | end) 15 | end) 16 | 17 | describe(".state", function() 18 | it('should be "idle"', function() 19 | assert.are.equals("idle", i.state) 20 | end) 21 | end) 22 | 23 | describe(".span", function () 24 | it("should be 1", function() 25 | assert.are.equals(1, i.span) 26 | end) 27 | end) 28 | 29 | describe(".height", function () 30 | it("should be 1", function() 31 | assert.are.equals(1, i.height) 32 | end) 33 | end) 34 | 35 | describe("stringify", function() 36 | it('should return "_ "', function() 37 | assert.are.equals("_ ", stringify(i)) 38 | end) 39 | end) 40 | 41 | describe("is_not_fallable()", function() 42 | it("should return true", function() 43 | assert.is_true(i:is_not_fallable()) 44 | end) 45 | end) 46 | 47 | describe("is_reducible()", function() 48 | it("should return false", function() 49 | assert.is_false(i:is_reducible()) 50 | end) 51 | end) 52 | end) 53 | -------------------------------------------------------------------------------- /test/block/q_block_utest.lua: -------------------------------------------------------------------------------- 1 | require("engine/test/bustedhelper") 2 | require("lib/block") 3 | 4 | describe('q_block', function() 5 | local q 6 | 7 | before_each(function() 8 | q = block_class("?") 9 | end) 10 | 11 | describe(".type", function() 12 | it('should be "?"', function() 13 | assert.are.equals("?", q.type) 14 | end) 15 | end) 16 | 17 | describe(".state", function() 18 | it('should be "idle"', function() 19 | assert.is_true(q.state == "idle") 20 | end) 21 | end) 22 | 23 | describe(".span", function () 24 | it("should be 1", function() 25 | assert.are.equals(1, q.span) 26 | end) 27 | end) 28 | 29 | describe(".height", function () 30 | it("should be 1", function() 31 | assert.are.equals(1, q.height) 32 | end) 33 | end) 34 | 35 | describe("stringify", function() 36 | it("should return '? '", function() 37 | assert.are.equals("? ", stringify(q)) 38 | end) 39 | end) 40 | 41 | describe("is_not_fallable()", function() 42 | it("should return true", function() 43 | assert.is_true(q:is_not_fallable()) 44 | end) 45 | end) 46 | 47 | describe("is_reducible()", function() 48 | it("should return false", function() 49 | assert.is_false(q:is_reducible()) 50 | end) 51 | end) 52 | end) 53 | -------------------------------------------------------------------------------- /test/block/s_block_utest.lua: -------------------------------------------------------------------------------- 1 | require("engine/test/bustedhelper") 2 | require("lib/block") 3 | 4 | describe('s_block', function() 5 | local s 6 | 7 | before_each(function() 8 | s = block_class("s") 9 | end) 10 | 11 | describe(".type", function() 12 | it('should be "s"', function() 13 | assert.are.equals("s", s.type) 14 | end) 15 | end) 16 | 17 | describe(".state", function() 18 | it('should be "idle"', function() 19 | assert.is_true(s.state == "idle") 20 | end) 21 | end) 22 | 23 | describe(".span", function () 24 | it("should be 1", function() 25 | assert.are.equals(1, s.span) 26 | end) 27 | end) 28 | 29 | describe(".height", function () 30 | it("should be 1", function() 31 | assert.are.equals(1, s.height) 32 | end) 33 | end) 34 | 35 | describe("stringify", function() 36 | it("should return 'S '", function() 37 | assert.are.equals("S ", stringify(s)) 38 | end) 39 | end) 40 | 41 | describe("is_not_fallable()", function() 42 | it("should return false", function() 43 | assert.is_false(s:is_not_fallable()) 44 | end) 45 | end) 46 | 47 | describe("is_reducible()", function() 48 | it("should return true", function() 49 | assert.is_true(s:is_reducible()) 50 | end) 51 | end) 52 | end) 53 | -------------------------------------------------------------------------------- /test/block/t_block_utest.lua: -------------------------------------------------------------------------------- 1 | require("engine/test/bustedhelper") 2 | require("lib/block") 3 | 4 | describe('t_block', function() 5 | local t 6 | 7 | before_each(function() 8 | t = block_class("t") 9 | end) 10 | 11 | describe(".type", function() 12 | it('should be "t"', function() 13 | assert.are.equals("t", t.type) 14 | end) 15 | end) 16 | 17 | describe(".state", function() 18 | it('should be "idle"', function() 19 | assert.is_true(t.state == "idle") 20 | end) 21 | end) 22 | 23 | describe(".span", function () 24 | it("should be 1", function() 25 | assert.are.equals(1, t.span) 26 | end) 27 | end) 28 | 29 | describe(".height", function () 30 | it("should be 1", function() 31 | assert.are.equals(1, t.height) 32 | end) 33 | end) 34 | 35 | describe("stringify", function() 36 | it("should return 'T '", function() 37 | assert.are.equals("T ", stringify(t)) 38 | end) 39 | end) 40 | 41 | describe("is_not_fallable()", function() 42 | it("should return false", function() 43 | assert.is_false(t:is_not_fallable()) 44 | end) 45 | end) 46 | 47 | describe("is_reducible()", function() 48 | it("should return true", function() 49 | assert.is_true(t:is_reducible()) 50 | end) 51 | end) 52 | end) 53 | -------------------------------------------------------------------------------- /test/block/x_block_utest.lua: -------------------------------------------------------------------------------- 1 | require("engine/test/bustedhelper") 2 | require("lib/block") 3 | 4 | describe('x_block', function() 5 | local x 6 | 7 | before_each(function() 8 | x = block_class("x") 9 | end) 10 | 11 | describe(".type", function() 12 | it('should be "x"', function() 13 | assert.are.equals("x", x.type) 14 | end) 15 | end) 16 | 17 | describe(".state", function() 18 | it('should be "idle"', function() 19 | assert.is_true(x.state == "idle") 20 | end) 21 | end) 22 | 23 | describe(".span", function () 24 | it("should be 1", function() 25 | assert.are.equals(1, x.span) 26 | end) 27 | end) 28 | 29 | describe(".height", function () 30 | it("should be 1", function() 31 | assert.are.equals(1, x.height) 32 | end) 33 | end) 34 | 35 | describe("stringify", function() 36 | it("should return 'X '", function() 37 | assert.are.equals("X ", stringify(x)) 38 | end) 39 | end) 40 | 41 | describe("is_not_fallable()", function() 42 | it("should return false", function() 43 | assert.is_false(x:is_not_fallable()) 44 | end) 45 | end) 46 | 47 | describe("is_reducible()", function() 48 | it("should return true", function() 49 | assert.is_true(x:is_reducible()) 50 | end) 51 | end) 52 | end) 53 | -------------------------------------------------------------------------------- /test/block/y_block_utest.lua: -------------------------------------------------------------------------------- 1 | require("engine/test/bustedhelper") 2 | require("lib/block") 3 | 4 | describe('y_block', function() 5 | local y 6 | 7 | before_each(function() 8 | y = block_class("y") 9 | end) 10 | 11 | describe(".type", function() 12 | it('should be "y"', function() 13 | assert.are.equals("y", y.type) 14 | end) 15 | end) 16 | 17 | describe(".state", function() 18 | it('should be "idle"', function() 19 | assert.is_true(y.state == "idle") 20 | end) 21 | end) 22 | 23 | describe(".span", function () 24 | it("should be 1", function() 25 | assert.are.equals(1, y.span) 26 | end) 27 | end) 28 | 29 | describe(".height", function () 30 | it("should be 1", function() 31 | assert.are.equals(1, y.height) 32 | end) 33 | end) 34 | 35 | describe("stringify", function() 36 | it("should return 'Y '", function() 37 | assert.are.equals("Y ", stringify(y)) 38 | end) 39 | end) 40 | 41 | describe("is_not_fallable()", function() 42 | it("should return false", function() 43 | assert.is_false(y:is_not_fallable()) 44 | end) 45 | end) 46 | 47 | describe("is_reducible()", function() 48 | it("should return true", function() 49 | assert.is_true(y:is_reducible()) 50 | end) 51 | end) 52 | end) 53 | -------------------------------------------------------------------------------- /test/block/z_block_utest.lua: -------------------------------------------------------------------------------- 1 | require("engine/test/bustedhelper") 2 | require("lib/block") 3 | 4 | describe('z_block', function() 5 | local z 6 | 7 | before_each(function() 8 | z = block_class("z") 9 | end) 10 | 11 | describe(".type", function() 12 | it('should be "z"', function() 13 | assert.are.equals("z", z.type) 14 | end) 15 | end) 16 | 17 | describe(".state", function() 18 | it('should be "idle"', function() 19 | assert.is_true(z.state == "idle") 20 | end) 21 | end) 22 | 23 | describe(".span", function () 24 | it("should be 1", function() 25 | assert.are.equals(1, z.span) 26 | end) 27 | end) 28 | 29 | describe(".height", function () 30 | it("should be 1", function() 31 | assert.are.equals(1, z.height) 32 | end) 33 | end) 34 | 35 | describe("stringify", function() 36 | it("should return 'Z '", function() 37 | assert.are.equals("Z ", stringify(z)) 38 | end) 39 | end) 40 | 41 | describe("is_not_fallable()", function() 42 | it("should return false", function() 43 | assert.is_false(z:is_not_fallable()) 44 | end) 45 | end) 46 | 47 | describe("is_reducible()", function() 48 | it("should return true", function() 49 | assert.is_true(z:is_reducible()) 50 | end) 51 | end) 52 | end) 53 | -------------------------------------------------------------------------------- /src/main_qpu_vs_qpu.lua: -------------------------------------------------------------------------------- 1 | require("lib/helpers") 2 | require("lib/effects") 3 | require("lib/board") 4 | require("lib/qpu") 5 | require("lib/game") 6 | 7 | local game = game_class() 8 | local qpu1_board, qpu2_board = board_class(cursor_class(), 3), board_class(cursor_class(), 78) 9 | qpu1_board.block_offset_target, qpu2_board.block_offset_target = { 3 + 24, 9 }, { 78 + 24, 9 } 10 | qpu1_board.attack_ion_target, qpu2_board.attack_ion_target = { 78 + 24, 9 }, { 3 + 24, 9 } 11 | local qpu1, qpu2 = qpu_class(qpu1_board, 1), qpu_class(qpu2_board, 1) 12 | 13 | game:add_player(qpu1, qpu1_board, qpu2_board) 14 | game:add_player(qpu2, qpu2_board, qpu1_board) 15 | 16 | function _init() 17 | game:init() 18 | end 19 | 20 | function _update60() 21 | game:update() 22 | 23 | if game:is_game_over() then 24 | if t() - game.game_over_time > 2 then 25 | qpu1_board.show_gameover_menu = true 26 | qpu2_board.show_gameover_menu = true 27 | 28 | if btnp(5) then -- x でリプレイ 29 | sfx(15) 30 | game:init() 31 | elseif btnp(4) then -- c でタイトルへ戻る 32 | jump('quantattack_title') 33 | end 34 | end 35 | end 36 | end 37 | 38 | function _draw() 39 | cls() 40 | 41 | game:render() 42 | 43 | print_outlined("time", 57, 106, 7, 0) 44 | print_outlined(game:elapsed_time_string(), 55, 114, 7, 0) 45 | end 46 | -------------------------------------------------------------------------------- /test/block/hash_block_utest.lua: -------------------------------------------------------------------------------- 1 | require("engine/test/bustedhelper") 2 | require("lib/block") 3 | 4 | describe('hash_block', function() 5 | local hash 6 | 7 | before_each(function() 8 | hash = block_class("#") 9 | end) 10 | 11 | describe(".type", function() 12 | it('should be #', function() 13 | assert.are.equals("#", hash.type) 14 | end) 15 | end) 16 | 17 | describe(".state", function() 18 | it('should be "idle"', function() 19 | assert.is_true(hash.state == "idle") 20 | end) 21 | end) 22 | 23 | describe(".span", function () 24 | it("should be 1", function() 25 | assert.are.equals(1, hash.span) 26 | end) 27 | end) 28 | 29 | describe(".height", function () 30 | it("should be 1", function() 31 | assert.are.equals(1, hash.height) 32 | end) 33 | end) 34 | 35 | describe("stringify", function() 36 | it("should return '# '", function() 37 | assert.are.equals("# ", stringify(hash)) 38 | end) 39 | end) 40 | 41 | describe("is_not_fallable()", function() 42 | it("should return false", function() 43 | assert.is_false(hash:is_not_fallable()) 44 | end) 45 | end) 46 | 47 | describe("is_reducible()", function() 48 | it("should return true", function() 49 | assert.is_true(hash:is_reducible()) 50 | end) 51 | end) 52 | end) 53 | -------------------------------------------------------------------------------- /test/block/swap_block_utest.lua: -------------------------------------------------------------------------------- 1 | require("engine/test/bustedhelper") 2 | require("test/test_helper") 3 | require("lib/block") 4 | 5 | describe('swap_block', function() 6 | local swap 7 | 8 | before_each(function() 9 | swap = swap_block(2) 10 | end) 11 | 12 | describe(".type", function() 13 | it('should be "swap"', function() 14 | assert.are.equals("swap", swap.type) 15 | end) 16 | end) 17 | 18 | describe(".state", function() 19 | it('should be "idle"', function() 20 | assert.is_true(swap.state == "idle") 21 | end) 22 | end) 23 | 24 | describe(".span", function () 25 | it("should be 1", function() 26 | assert.are.equals(1, swap.span) 27 | end) 28 | end) 29 | 30 | describe(".height", function () 31 | it("should be 1", function() 32 | assert.are.equals(1, swap.height) 33 | end) 34 | end) 35 | 36 | describe("stringify", function() 37 | it("should return 'X '", function() 38 | assert.are.equals("X ", stringify(swap)) 39 | end) 40 | end) 41 | 42 | describe("is_not_fallable()", function() 43 | it("should return false", function() 44 | assert.is_false(swap:is_not_fallable()) 45 | end) 46 | end) 47 | 48 | describe("is_reducible()", function() 49 | it("should return true", function() 50 | assert.is_true(swap:is_reducible()) 51 | end) 52 | end) 53 | end) 54 | -------------------------------------------------------------------------------- /test/block/cnot_x_block_utest.lua: -------------------------------------------------------------------------------- 1 | require("engine/test/bustedhelper") 2 | require("test/test_helper") 3 | require("lib/block") 4 | 5 | describe('cnot_x_block', function() 6 | local cnot_x 7 | 8 | before_each(function() 9 | cnot_x = cnot_x_block(2) 10 | end) 11 | 12 | describe(".type", function() 13 | it('should be "cnot_x"', function() 14 | assert.are.equals("cnot_x", cnot_x.type) 15 | end) 16 | end) 17 | 18 | describe(".state", function() 19 | it('should be "idle"', function() 20 | assert.is_true(cnot_x.state == "idle") 21 | end) 22 | end) 23 | 24 | describe(".span", function () 25 | it("should be 1", function() 26 | assert.are.equals(1, cnot_x.span) 27 | end) 28 | end) 29 | 30 | describe(".height", function () 31 | it("should be 1", function() 32 | assert.are.equals(1, cnot_x.height) 33 | end) 34 | end) 35 | 36 | describe("stringify", function() 37 | it("should return '+ '", function() 38 | assert.are.equals("+ ", stringify(cnot_x)) 39 | end) 40 | end) 41 | 42 | describe("is_not_fallable()", function() 43 | it("should return false", function() 44 | assert.is_false(cnot_x:is_not_fallable()) 45 | end) 46 | end) 47 | 48 | describe("is_reducible()", function() 49 | it("should return true", function() 50 | assert.is_true(cnot_x:is_reducible()) 51 | end) 52 | end) 53 | end) 54 | -------------------------------------------------------------------------------- /test/block/control_block_utest.lua: -------------------------------------------------------------------------------- 1 | require("engine/test/bustedhelper") 2 | require("test/test_helper") 3 | require("lib/block") 4 | 5 | describe('control_block', function() 6 | local control 7 | 8 | before_each(function() 9 | control = control_block(2) 10 | end) 11 | 12 | describe(".type", function() 13 | it('should be "control"', function() 14 | assert.are.equals("control", control.type) 15 | end) 16 | end) 17 | 18 | describe(".state", function() 19 | it('should be "idle"', function() 20 | assert.is_true(control.state == "idle") 21 | end) 22 | end) 23 | 24 | describe(".span", function () 25 | it("should be 1", function() 26 | assert.are.equals(1, control.span) 27 | end) 28 | end) 29 | 30 | describe(".height", function () 31 | it("should be 1", function() 32 | assert.are.equals(1, control.height) 33 | end) 34 | end) 35 | 36 | describe("stringify", function() 37 | it("should return '● '", function() 38 | assert.are.equals("● ", stringify(control)) 39 | end) 40 | end) 41 | 42 | describe("is_not_fallable()", function() 43 | it("should return false", function() 44 | assert.is_false(control:is_not_fallable()) 45 | end) 46 | end) 47 | 48 | describe("is_reducible()", function() 49 | it("should return true", function() 50 | assert.is_true(control:is_reducible()) 51 | end) 52 | end) 53 | end) 54 | -------------------------------------------------------------------------------- /scripts/test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Configuration 4 | game_src_path="$(dirname "$0")/../src" 5 | picoboots_scripts_path="$(dirname "$0")/../pico-boots/scripts" 6 | 7 | help() { 8 | echo "Test pico-boots modules with busted 9 | This is essentially a proxy script for scripts/test_scripts.sh that avoids 10 | passing src/engine/FOLDER every time we want to test a group of scripts. 11 | Dependencies: 12 | - busted (must be in PATH) 13 | - luacov (must be in PATH) 14 | " 15 | usage 16 | } 17 | 18 | usage() { 19 | echo "Usage: test.sh 20 | EXTRA PARAMETERS 21 | Any extra parameter is passed to scripts/test_scripts.sh (besides the ROOT arguments 22 | and --cov-config parameter). 23 | Enter 'scripts/test_scripts.sh --help' (from pico-boots root) for more information. 24 | -h, --help Show this help message 25 | " 26 | } 27 | 28 | # Read arguments 29 | # https://stackoverflow.com/questions/192249/how-do-i-parse-command-line-arguments-in-bash 30 | while [[ $# -gt 0 ]]; do 31 | case $1 in 32 | -h | --help ) 33 | help 34 | exit 0 35 | ;; 36 | -* ) # we started adding options 37 | # since we don't support "--" for final positional arguments, just pass all the rest to test_scripts.sh 38 | break 39 | ;; 40 | * ) 41 | shift # past argument 42 | ;; 43 | esac 44 | done 45 | 46 | "$picoboots_scripts_path/test_scripts.sh" test --lua-root "$game_src_path" $@ 47 | -------------------------------------------------------------------------------- /scripts/build_and_install_all_cartridges.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Build all game cartridges, including data cartridges and install them in PICO-8 carts folder. 4 | 5 | # Configuration: paths 6 | game_scripts_path="$(dirname "$0")" 7 | 8 | help() { 9 | echo "Build and install all PICO-8 cartridges with the passed config." 10 | usage 11 | } 12 | 13 | usage() { 14 | echo "Usage: build_and_install_all_cartridges.sh [CONFIG] 15 | ARGUMENTS 16 | CONFIG Build config. Determines defined preprocess symbols. 17 | (default: 'debug') 18 | -h, --help Show this help message 19 | " 20 | } 21 | 22 | # Default parameters 23 | config='debug' 24 | 25 | # Read arguments 26 | # https://stackoverflow.com/questions/192249/how-do-i-parse-command-line-arguments-in-bash 27 | while [[ $# -gt 0 ]]; do 28 | case $1 in 29 | -h | --help ) 30 | help 31 | exit 0 32 | ;; 33 | -* ) # unknown option 34 | echo "Unknown option: '$1'" 35 | usage 36 | exit 1 37 | ;; 38 | * ) # store positional argument for later 39 | positional_args+=("$1") 40 | shift # past argument 41 | ;; 42 | esac 43 | done 44 | 45 | if ! [[ ${#positional_args[@]} -ge 0 && ${#positional_args[@]} -le 1 ]]; then 46 | echo "Wrong number of positional arguments: found ${#positional_args[@]}, expected 0 or 1." 47 | echo "Passed positional arguments: ${positional_args[@]}" 48 | usage 49 | exit 1 50 | fi 51 | 52 | if [[ ${#positional_args[@]} -ge 1 ]]; then 53 | config="${positional_args[0]}" 54 | fi 55 | 56 | "$game_scripts_path/build_all_cartridges.sh" "$config" 57 | "$game_scripts_path/install_all_cartridges.sh" "$config" 58 | -------------------------------------------------------------------------------- /test/particle_utest.lua: -------------------------------------------------------------------------------- 1 | require("engine/test/bustedhelper") 2 | require("lib/helpers") 3 | require("lib/effects") 4 | require("lib/board") 5 | 6 | describe('particle', function() 7 | before_each(function() 8 | for i = 1, 100 do 9 | particles:update_all() 10 | end 11 | end) 12 | 13 | describe('create', function() 14 | it("creates a particle", function() 15 | particles:create({ 1, 1 }, "1,1,7,5,1,1,1,1,10") 16 | particles:create({ 2, 2 }, "2,2,8,11,1,1,1,1,20") 17 | 18 | assert.are.equal(2, #particles.all) 19 | 20 | assert.are.equal(1, particles.all[1]._x) 21 | assert.are.equal(1, particles.all[1]._y) 22 | assert.are.equal(1, tonum(particles.all[1]._radius)) 23 | assert.are.equal(colors["white"], tonum(particles.all[1]._color)) 24 | assert.are.equal(colors["dark_gray"], tonum(particles.all[1]._color_fade)) 25 | assert.are.equal(0, particles.all[1]._tick) 26 | 27 | assert.are.equal(2, particles.all[2]._x) 28 | assert.are.equal(2, particles.all[2]._y) 29 | assert.are.equal(2, tonum(particles.all[2]._radius)) 30 | assert.are.equal(colors["red"], tonum(particles.all[2]._color)) 31 | assert.are.equal(colors["green"], tonum(particles.all[2]._color_fade)) 32 | assert.are.equal(0, particles.all[2]._tick) 33 | end) 34 | end) 35 | 36 | describe('update', function() 37 | it("updates all partciles", function() 38 | particles:create({ 1, 1 }, "1,1,7,5,1,1,1,1,20") 39 | 40 | assert.are.equal(1, #particles.all) 41 | 42 | particles:update_all() 43 | 44 | assert.are.equal(1, particles.all[1]._tick) 45 | end) 46 | end) 47 | 48 | describe('render', function() 49 | it("renders all particles", function() 50 | particles:create({ 1, 1 }, "1,1,7,5,1,1,1,1,20") 51 | 52 | assert.has_no.errors(function() 53 | particles:render_all() 54 | end) 55 | end) 56 | end) 57 | end) 58 | -------------------------------------------------------------------------------- /test/board/board_is_empty_utest.lua: -------------------------------------------------------------------------------- 1 | ---@diagnostic disable: lowercase-global 2 | 3 | require("engine/test/bustedhelper") 4 | require("test/test_helper") 5 | require("lib/board") 6 | 7 | describe('board:is_empty()', function() 8 | local board 9 | 10 | before_each(function() 11 | board = board_class() 12 | end) 13 | 14 | it("ブロックのない場所では true を返す", function() 15 | assert.is_true(board:is_empty(1, 1)) 16 | end) 17 | 18 | it("ブロックのある場所では false を返す", function() 19 | board:put(1, 1, block_class("h")) 20 | assert.is_false(board:is_empty(1, 1)) 21 | end) 22 | 23 | it("入れ替え中の場所では false を返す", function() 24 | board:put(2, 1, block_class("h")) 25 | board:swap(1, 1) 26 | assert.is_false(board:is_empty(1, 1)) 27 | end) 28 | 29 | it("CNOT の上では false を返す", function() 30 | local control = block_class("control") 31 | control.other_x = 4 32 | board:put(1, 1, control) 33 | 34 | local cnot_x = block_class("cnot_x") 35 | cnot_x.other_x = 1 36 | board:put(4, 1, cnot_x) 37 | 38 | assert.is_false(board:is_empty(1, 1)) 39 | assert.is_false(board:is_empty(2, 1)) 40 | assert.is_false(board:is_empty(3, 1)) 41 | assert.is_false(board:is_empty(4, 1)) 42 | end) 43 | 44 | it("SWAP の上では false を返す", function() 45 | local swap_left = block_class("swap") 46 | swap_left.other_x = 4 47 | board:put(1, 1, swap_left) 48 | 49 | local swap_right = block_class("swap") 50 | swap_right.other_x = 1 51 | board:put(4, 1, swap_right) 52 | 53 | assert.is_false(board:is_empty(1, 1)) 54 | assert.is_false(board:is_empty(2, 1)) 55 | assert.is_false(board:is_empty(3, 1)) 56 | assert.is_false(board:is_empty(4, 1)) 57 | end) 58 | 59 | it("おじゃまブロックの上では false を返す", function() 60 | board:put(1, 1, garbage_block(3, 1)) 61 | 62 | assert.is_false(board:is_empty(1, 1)) 63 | assert.is_false(board:is_empty(2, 1)) 64 | assert.is_false(board:is_empty(3, 1)) 65 | end) 66 | end) 67 | -------------------------------------------------------------------------------- /scripts/install_all_cartridges.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Install all cartridges for the game, including data cartridges, to the default 4 | # PICO-8 carts location 5 | # This is required if you need to play with multiple carts, 6 | # as other carts will only be loaded in PICO-8 carts location 7 | 8 | # Usage: install_all_cartridges.sh config [--itest] 9 | # config build config (e.g. 'debug' or 'release') 10 | # -i, --itest pass this option to build an itest instead of a normal game cartridge 11 | 12 | # Currently only supported on Linux 13 | 14 | # Configuration: paths 15 | game_scripts_path="$(dirname "$0")" 16 | data_path="$(dirname "$0")/../data" 17 | 18 | # check that source and output paths have been provided 19 | if ! [[ $# -ge 1 && $# -le 3 ]] ; then 20 | echo "install_all_cartridges.sh takes 1 or 2 params + option value, provided $#: 21 | \$1: config ('debug', 'release', etc.) 22 | -i, --itest: Pass this option to build an itest instead of a normal game cartridge." 23 | exit 1 24 | fi 25 | 26 | config="$1"; shift 27 | # ! This is a short version for the usual while-case syntax, but in counterpart 28 | # ! it doesn't support reordering (--itest must be after config) 29 | if [[ $1 == '-i' || $1 == '--itest' ]]; then 30 | itest=true 31 | shift 32 | fi 33 | 34 | if [[ "$itest" == true ]]; then 35 | # itest cartridges enforce special config 'itest' and ignore passed config 36 | config='itest' 37 | options='--itest' 38 | else 39 | options='' 40 | fi 41 | 42 | # cartridges.txt lists cartridge names, one line per cartridge 43 | # newlines act like separators for iteration just like spaces, 44 | # so this is equivalent to `cartridge_list="titlemenu stage_intro ..."` 45 | cartridge_list=`cat "$data_path/cartridges.txt"` 46 | 47 | for cartridge in $cartridge_list; do 48 | "$game_scripts_path/install_single_cartridge.sh" "$cartridge" "$config" $options 49 | done 50 | 51 | "$game_scripts_path/install_data_cartridges_with_merging.sh" "$config" 52 | -------------------------------------------------------------------------------- /src/tutorial/ion.lua: -------------------------------------------------------------------------------- 1 | ---@diagnostic disable: global-in-nil-env, lowercase-global 2 | require("lib/helpers") 3 | 4 | local ion = new_class() 5 | 6 | function ion._init(_ENV) 7 | _x = 84 8 | _y = 36 9 | _dx = 0 10 | _dy = 0 11 | end 12 | 13 | function ion.update(_ENV) 14 | if _state == ":appear" then 15 | if _tick == _max_tick then 16 | _state = "idle" 17 | _appear_callback() 18 | return 19 | end 20 | 21 | _dx, _dy = 0, 0 22 | _x, _y = _quadratic_bezier(_ENV, 64, 128, 84), _quadratic_bezier(_ENV, 128, 64, 36) 23 | 24 | -- イオン君のしっぽを追加 25 | if ceil_rnd(10) > 7 then 26 | particles:create( 27 | { _x, _y }, 28 | "3,1,12,7,0,0,0,0,20" 29 | ) 30 | end 31 | elseif _state == ":shake" then 32 | if _tick == _max_tick then 33 | _state = "idle" 34 | if _shake_callback then 35 | _shake_callback() 36 | end 37 | end 38 | 39 | _dx = 0 40 | _dy = cos(t() / 0.2) * 4 41 | else -- state == "idle" 42 | _dx = cos(t() / 2) * 1.5 43 | _dy = sin(t() / 2.5) * 1.5 44 | end 45 | 46 | _tick = _tick + 1 47 | end 48 | 49 | function ion.draw(_ENV) 50 | local x = _x + _dx 51 | local y = _y + _dy 52 | local angle = t() 53 | 54 | fillp(23130.5) 55 | circfill(x, y, 8 + 2 * sin(angle), 12) 56 | fillp() 57 | circfill(x, y, 6 + 2 * sin(1.5 * angle), 12) 58 | circfill(x, y, 5 + sin(2.5 * angle), 7) 59 | end 60 | 61 | -- 画面外から定位置に登場 62 | function ion.appear(_ENV, callback) 63 | _state = ":appear" 64 | _tick = 0 65 | _max_tick = 60 66 | _appear_callback = callback 67 | sfx(20) 68 | end 69 | 70 | -- 左右に素早くゆれる 71 | function ion.shake(_ENV, callback) 72 | _state = ":shake" 73 | _tick = 0 74 | _max_tick = 30 75 | _shake_callback = callback 76 | sfx(20) 77 | end 78 | 79 | -- TODO: attack_ion と共通の関数にする 80 | function ion._quadratic_bezier(_ENV, from, mid, to) 81 | local t = _tick / 60 82 | return (1 - t) * (1 - t) * from + 2 * (1 - t) * t * mid + t * t * to 83 | end 84 | 85 | return ion 86 | -------------------------------------------------------------------------------- /src/lib/helpers.lua: -------------------------------------------------------------------------------- 1 | --- ヘルパ関数 2 | 3 | --- 1 以上 num 以下の乱数を返す 4 | function ceil_rnd(num) 5 | return flr(rnd(num)) + 1 6 | end 7 | 8 | --- 角の丸い箱を描く 9 | function draw_rounded_box(x0, y0, x1, y1, border_color, fill_color) 10 | line(x0 + 1, y0, x1 - 1, y0, border_color) 11 | line(x1, y0 + 1, x1, y1 - 1, border_color) 12 | line(x1 - 1, y1, x0 + 1, y1, border_color) 13 | line(x0, y1 - 1, x0, y0 + 1, border_color) 14 | 15 | if fill_color then 16 | rectfill(x0 + 1, y0 + 1, x1 - 1, y1 - 1, fill_color) 17 | end 18 | end 19 | 20 | --- アウトライン付き文字を表示 21 | function print_outlined(str, x, y, color, border_color) 22 | if border_color ~= 0 then 23 | for dx = -2, 2 do 24 | for dy = -2, 2 do 25 | print(str, x + dx, y + dy, 0) 26 | end 27 | end 28 | end 29 | for dx = -1, 1 do 30 | for dy = -1, 1 do 31 | print(str, x + dx, y + dy, border_color or 12) 32 | end 33 | end 34 | 35 | print(str, x, y, color) 36 | end 37 | 38 | local function new(cls, ...) 39 | local self = setmetatable({}, cls) 40 | self:_init(...) 41 | return self 42 | end 43 | 44 | --- クラスを作る 45 | function new_class() 46 | local class = {} 47 | class.__index = class 48 | 49 | setmetatable(class, { 50 | __index = _ENV, 51 | __call = new 52 | }) 53 | 54 | return class 55 | end 56 | 57 | --- 継承したクラスを返す 58 | function derived_class(base_class) 59 | local class = {} 60 | class.__index = class 61 | 62 | setmetatable(class, { 63 | __index = base_class, 64 | __call = new 65 | }) 66 | 67 | return class 68 | end 69 | 70 | --- カートを呼び出す 71 | function jump(name, breadcrumb, param) 72 | load(name .. ".p8", breadcrumb, param) 73 | load("#" .. name, breadcrumb, param) 74 | end 75 | 76 | --- unpack & split 77 | function unpack_split(...) 78 | return unpack(split(...)) 79 | end 80 | 81 | --- map 関数 82 | function transform(t, func) 83 | local transformed_t = {} 84 | for key, value in pairs(t) do 85 | transformed_t[key] = func(value) 86 | end 87 | return transformed_t 88 | end 89 | -------------------------------------------------------------------------------- /scripts/build_all_cartridges.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | #!/bin/bash 4 | 5 | # Build all game cartridges. 6 | # This essentially calls build_single_cartridge.sh on each cartridge. 7 | 8 | # Configuration: paths 9 | game_scripts_path="$(dirname "$0")" 10 | data_path="$(dirname "$0")/../data" 11 | 12 | help() { 13 | echo "Build a PICO-8 cartridge with the passed config." 14 | usage 15 | } 16 | 17 | usage() { 18 | echo "Usage: build_all_cartridges.sh CARTRIDGE_SUFFIX [CONFIG] 19 | ARGUMENTS 20 | CONFIG Build config. Determines defined preprocess symbols. 21 | (default: 'debug') 22 | -h, --help Show this help message 23 | " 24 | } 25 | 26 | # Default parameters 27 | config='debug' 28 | 29 | # Read arguments 30 | # https://stackoverflow.com/questions/192249/how-do-i-parse-command-line-arguments-in-bash 31 | roots=() 32 | while [[ $# -gt 0 ]]; do 33 | case $1 in 34 | -h | --help ) 35 | help 36 | exit 0 37 | ;; 38 | -* ) # unknown option 39 | echo "Unknown option: '$1'" 40 | usage 41 | exit 1 42 | ;; 43 | * ) # store positional argument for later 44 | positional_args+=("$1") 45 | shift # past argument 46 | ;; 47 | esac 48 | done 49 | 50 | if ! [[ ${#positional_args[@]} -ge 0 && ${#positional_args[@]} -le 1 ]]; then 51 | echo "Wrong number of positional arguments: found ${#positional_args[@]}, expected 0 or 1." 52 | echo "Passed positional arguments: ${positional_args[@]}" 53 | usage 54 | exit 1 55 | fi 56 | 57 | if [[ ${#positional_args[@]} -ge 1 ]]; then 58 | config="${positional_args[0]}" 59 | fi 60 | 61 | # cartridges.txt lists cartridge names, one line per cartridge 62 | # newlines act like separators for iteration just like spaces, 63 | # so this is equivalent to `cartridge_list="titlemenu stage_intro ..."` 64 | cartridge_list=`cat "$data_path/cartridges.txt"` 65 | 66 | for cartridge in $cartridge_list; do 67 | "$game_scripts_path/build_single_cartridge.sh" "$cartridge" "$config" 68 | done 69 | -------------------------------------------------------------------------------- /src/lib/replay.lua: -------------------------------------------------------------------------------- 1 | do 2 | local original_functions = { 3 | _update60 = _update60, 4 | _draw = _draw, 5 | btn = btn, 6 | btnp = btnp 7 | } 8 | 9 | local bstate, pstate, addr, is_replay = {}, {}, 0x8000, false 10 | 11 | local function updatebtns() 12 | for i = 0, 5 do 13 | pstate[i] = bstate[i] 14 | end 15 | if is_replay then 16 | local mask = peek(addr) 17 | addr = addr + 1 18 | if (mask == 0xff) then 19 | run() 20 | end 21 | for i = 0, 5 do 22 | bstate[i] = mask & (1 << i) ~= 0 23 | end 24 | else 25 | local mask = 0 26 | for i = 0, 5 do 27 | bstate[i] = original_functions.btn(i) 28 | if (bstate[i]) then 29 | mask = mask|(1 << i) 30 | end 31 | end 32 | if addr < 0x8000 + 0x42ff then 33 | poke(addr, mask) 34 | addr = addr + 1 35 | end 36 | end 37 | end 38 | 39 | local function doreplay() 40 | poke(addr, 0xff) 41 | memcpy(0, 0x8000, 0x4300) 42 | cstore(0, 0, 0x4300, "quantattack_replay.p8") 43 | dset(63, 1) 44 | run() 45 | end 46 | 47 | cartdata = function() 48 | end 49 | 50 | is_replay = dget(63) == 1 51 | if not is_replay then 52 | local seed = rnd(0xffff.ffff) 53 | poke4(addr, seed) 54 | addr = addr + 4 55 | srand(seed) 56 | menuitem(5, "replay", doreplay) 57 | else 58 | reload(0, 0, 0x4300, "quantattack_replay.p8") 59 | memcpy(0x8000, 0, 0x4300) 60 | reload(0, 0, 0x4300) 61 | local seed = peek4(addr) 62 | addr = addr + 4 63 | srand(seed) 64 | menuitem(5, "end replay", 65 | function() 66 | dset(63, 0) 67 | run() 68 | end) 69 | end 70 | 71 | _update60 = function() 72 | updatebtns() 73 | original_functions._update60() 74 | end 75 | 76 | btn = function(i) 77 | return bstate[i] 78 | end 79 | 80 | btnp = function(i) 81 | return bstate[i] and not pstate[i] 82 | end 83 | 84 | _draw = function() 85 | original_functions._draw() 86 | if is_replay then 87 | print("replay", 1, 6, 8) 88 | end 89 | end 90 | end 91 | -------------------------------------------------------------------------------- /src/tutorial/game.lua: -------------------------------------------------------------------------------- 1 | ---@diagnostic disable: global-in-nil-env, lowercase-global 2 | 3 | require("lib/helpers") 4 | 5 | local game = new_class() 6 | local _state 7 | 8 | function game._init(_ENV) 9 | _state = ":initial" 10 | show_board = false 11 | move_cursor = false 12 | end 13 | 14 | function game.update(_ENV) 15 | if _state == ":initial" then 16 | -- NOP 17 | elseif _state == ":raise_stack" then 18 | board.raised_dots = board.raised_dots + 1 19 | 20 | if _rows_raised < 8 then 21 | if board.raised_dots == 8 then 22 | _rows_raised = _rows_raised + 1 23 | board.raised_dots = 0 24 | board:shift_all_blocks_up() 25 | 26 | for x = 1, board.cols do 27 | local block_type, other_x = unpack_split(_board_data[_rows_raised][x]) 28 | local new_block = block_class(block_type) 29 | new_block.other_x = other_x 30 | board:put(x, 0, new_block) 31 | end 32 | end 33 | else 34 | _state = "idle" 35 | _raise_stack_callback() 36 | end 37 | else 38 | player:update(board) 39 | 40 | if move_cursor then 41 | if player.left then 42 | sfx(8) 43 | cursor:move_left() 44 | end 45 | if player.right then 46 | sfx(8) 47 | cursor:move_right(board.cols) 48 | end 49 | if player.up then 50 | sfx(8) 51 | cursor:move_up(board.rows) 52 | end 53 | if player.down then 54 | sfx(8) 55 | cursor:move_down() 56 | end 57 | if player.x and board:swap(cursor.x, cursor.y) then 58 | sfx(10) 59 | end 60 | end 61 | end 62 | 63 | -- すべてのモードに共通な update 処理 64 | board:update(_ENV, player) 65 | cursor:update() 66 | 67 | ripple:update_all() 68 | particles:update_all() 69 | bubbles:update_all() 70 | end 71 | 72 | function game.render(_ENV) 73 | if _state ~= ":initial" then 74 | board:render() 75 | end 76 | particles:render_all() 77 | bubbles:render_all() 78 | end 79 | 80 | function game.raise_stack(_ENV, board_data, callback) 81 | _state = ":raise_stack" 82 | _rows_raised = 0 83 | _board_data = board_data 84 | _raise_stack_callback = callback or function() end 85 | 86 | for x = 1, board.cols do 87 | for y = 0, board.rows do 88 | board:put(x, y, block_class("i")) 89 | end 90 | end 91 | end 92 | 93 | return game 94 | -------------------------------------------------------------------------------- /src/main_endless.lua: -------------------------------------------------------------------------------- 1 | require("lib/helpers") 2 | require("lib/effects") 3 | require("lib/board") 4 | require("lib/game") 5 | require("lib/player") 6 | require("lib/high_score") 7 | 8 | -- ハイスコア関係 9 | local high_score = high_score_class(1) 10 | local current_high_score 11 | 12 | local board = board_class() 13 | board.attack_ion_target = { 85, 30 } 14 | 15 | local player = player_class() 16 | local game = game_class() 17 | local last_steps = 0 18 | 19 | game:add_player(player, board) 20 | 21 | function _init() 22 | current_high_score = high_score:get() 23 | game:init() 24 | end 25 | 26 | function _update60() 27 | game:update() 28 | 29 | if board.steps > last_steps then 30 | -- 10 ステップごとに 31 | -- * おじゃまブロックを降らせる (最大 10 段) 32 | -- * ブロックをせり上げるスピードを上げる 33 | if board.steps > 0 and board.steps % 10 == 0 then 34 | if game.auto_raise_frame_count > 10 then 35 | game.auto_raise_frame_count = game.auto_raise_frame_count - 1 36 | end 37 | board:send_garbage(nil, 6, board.steps / 10 < 11 and board.steps / 10 or 10) 38 | end 39 | last_steps = board.steps 40 | end 41 | 42 | if game:is_game_over() then 43 | if t() - game.game_over_time > 2 then 44 | if not board.show_gameover_menu then 45 | if high_score:put(player.score) then 46 | sfx(22) 47 | sash:create("high score!,7,8") 48 | end 49 | end 50 | 51 | board.show_gameover_menu = true 52 | if btnp(5) then -- x でリプレイ 53 | sfx(15) 54 | current_high_score = high_score:get() 55 | game:init() 56 | elseif btnp(4) then -- c でタイトルへ戻る 57 | jump('quantattack_title') 58 | end 59 | end 60 | end 61 | 62 | sash:update_all() 63 | end 64 | 65 | function _draw() 66 | cls() 67 | 68 | game:render() 69 | 70 | local base_x = board.offset_x * 2 + board.width 71 | 72 | -- スコア表示 73 | print_outlined("score " .. tostr(player.score, 0x2), base_x, 16, 7, 0) 74 | print_outlined("hi-score " .. tostr(current_high_score, 0x2), base_x, 24, 7, 0) 75 | print_outlined(board.steps .. " steps", base_x, 38, 7, 0) 76 | 77 | if not game:is_game_over() then 78 | spr(99, base_x, 109) 79 | print_outlined("swap blocks", 81, 110, 7, 0) 80 | spr(112, base_x, 119) 81 | print_outlined("raise blocks", 81, 120, 7, 0) 82 | end 83 | 84 | sash:render_all() 85 | end 86 | 87 | require("lib/replay") 88 | -------------------------------------------------------------------------------- /test/sash_utest.lua: -------------------------------------------------------------------------------- 1 | require("engine/test/bustedhelper") 2 | require("engine/render/color") 3 | require("test/test_helper") 4 | require("lib/helpers") 5 | require("lib/effects") 6 | 7 | describe('sash', function() 8 | describe('create', function() 9 | before_each(function() 10 | sash:create('sash,7,8') 11 | end) 12 | 13 | describe('all', function () 14 | it('text プロパティを持つ', function() 15 | assert.are.equal('sash', sash.all[1].text) 16 | end) 17 | 18 | it('text_color プロパティを持つ', function() 19 | assert.are.equal(colors.white, tonum(sash.all[1].text_color)) 20 | end) 21 | 22 | it('background_color プロパティを持つ', function() 23 | assert.are.equal(colors.red, tonum(sash.all[1].background_color)) 24 | end) 25 | 26 | it('文字列がスクリーン外', function() 27 | assert.are.equal(-16, sash.all[1].text_x) 28 | end) 29 | 30 | it('背景なし', function () 31 | assert.are.equal(0, sash.all[1].background_height) 32 | end) 33 | 34 | it('状態が :slidein', function() 35 | assert.are.equal(':slidein', sash.all[1].state) 36 | end) 37 | end) 38 | end) 39 | 40 | describe('update_all', function() 41 | it('text_x を更新', function () 42 | sash:create('sash,0,0') 43 | 44 | sash:update_all() 45 | 46 | assert.are.equal(-16 + 5, sash.all[1].text_x) 47 | end) 48 | 49 | it('何度か update すると中央でストップ', function () 50 | sash:create('sash,0,0') 51 | 52 | while (sash.all[1].text_x < sash.all[1].text_center_x) do 53 | sash:update_all() 54 | end 55 | 56 | assert.are.equal(':stop', sash.all[1].state) 57 | end) 58 | 59 | it('画面右端から消えると :finished 状態になる', function () 60 | sash:create('sash,0,0') 61 | 62 | while (sash.all[1].text_x <= 127) do 63 | pico8.frames = pico8.frames + 1 64 | sash:update_all() 65 | end 66 | 67 | assert.are.equal(':finished', sash.all[1].state) 68 | end) 69 | end) 70 | 71 | describe('render_all', function() 72 | describe('sash がない場合', function() 73 | it('エラーなく描画できる', function() 74 | assert.has_no.errors(function () 75 | sash:render_all() 76 | end) 77 | end) 78 | end) 79 | 80 | describe('sash がある場合', function() 81 | before_each(function() 82 | sash:create('sash,0,0') 83 | end) 84 | 85 | it('エラーなく描画できる', function() 86 | assert.has_no.errors(function () 87 | sash:render_all() 88 | end) 89 | end) 90 | end) 91 | end) 92 | end) 93 | -------------------------------------------------------------------------------- /scripts/install_single_cartridge.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # Install a specific cartridge for the game to the default 3 | # PICO-8 carts location 4 | # This is required if you need to play with multiple carts, 5 | # as other carts will only be loaded in PICO-8 carts location 6 | # ! This does not install data and is not useful on its own, 7 | # make sure to use install_single_cartridge_with_data.sh or 8 | # to manually copy data cartridges after this. 9 | 10 | # Usage: install_single_cartridge.sh cartridge_suffix config [--itest] 11 | # cartridge_suffix see data/cartridges.txt for the list of cartridge names 12 | # config build config (e.g. 'debug' or 'release'. Default: 'debug') 13 | # -i, --itest pass this option to build an itest instead of a normal game cartridge 14 | 15 | # Currently only supported on Linux 16 | 17 | # Configuration: paths 18 | data_path="$(dirname "$0")/../data" 19 | 20 | # check that source and output paths have been provided 21 | if ! [[ $# -ge 1 && $# -le 4 ]] ; then 22 | echo "install_single_cartridge.sh takes 1 to 3 params + option value, provided $#: 23 | \$1: cartridge_suffix (see data/cartridges.txt for the list of cartridge names) 24 | \$2: config ('debug', 'release', etc. Default: 'debug') 25 | -i, --itest: Pass this option to build an itest instead of a normal game cartridge." 26 | exit 1 27 | fi 28 | 29 | # Configuration: cartridge 30 | cartridge_stem="quantattack" 31 | version=`cat "$data_path/version.txt"` 32 | cartridge_suffix="$1"; shift 33 | config="$1"; shift 34 | # ! This is a short version for the usual while-case syntax, but in counterpart 35 | # ! it doesn't support reordering (--itest must be after config) 36 | if [[ $1 == '-i' || $1 == '--itest' ]]; then 37 | itest=true 38 | shift 39 | fi 40 | 41 | if [[ "$itest" == true ]]; then 42 | # itest cartridges enforce special config 'itest' and ignore passed config 43 | config='itest' 44 | cartridge_extra_suffix='itest_all_' 45 | else 46 | cartridge_extra_suffix='' 47 | fi 48 | 49 | output_path="build/v${version}_${config}" 50 | cartridge_filepath="${output_path}/${cartridge_stem}_${cartridge_extra_suffix}${cartridge_suffix}.p8${suffix}" 51 | # Linux only 52 | carts_dirpath="$HOME/.lexaloffle/pico-8/carts" 53 | install_dirpath="${carts_dirpath}/${cartridge_stem}/v${version}_${config}" 54 | 55 | if [[ ! -f "${cartridge_filepath}" ]]; then 56 | echo "File ${cartridge_filepath} could not be found, cannot install. Make sure you built it first." 57 | exit 1 58 | fi 59 | 60 | mkdir -p "${install_dirpath}" 61 | 62 | echo "Installing ${cartridge_filepath} in ${install_dirpath} ..." 63 | # trailing slash just to make sure we copy to a directory 64 | cp "${cartridge_filepath}" "${install_dirpath}/" 65 | 66 | echo "Done." 67 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Lua.diagnostics.globals": [ 3 | "board", 4 | "_init", 5 | "describe", 6 | "before_each", 7 | "it", 8 | "new_class", 9 | "pal", 10 | "spr", 11 | "lfs", 12 | "cls", 13 | "_draw", 14 | "btnp", 15 | "sfx", 16 | "_update60", 17 | "fps60", 18 | "rnd", 19 | "flr", 20 | "add", 21 | "foreach", 22 | "del", 23 | "circfill", 24 | "line", 25 | "rectfill", 26 | "stringify", 27 | "min", 28 | "max", 29 | "color", 30 | "cursor", 31 | "tile_size", 32 | "stat", 33 | "split", 34 | "transform", 35 | "tonum", 36 | "screen_height", 37 | "colors", 38 | "ceil", 39 | "cnot_x_block", 40 | "control_block", 41 | "garbage_block", 42 | "garbage_match_block", 43 | "h_block", 44 | "i_block", 45 | "s_block", 46 | "swap_block", 47 | "t_block", 48 | "x_block", 49 | "y_block", 50 | "z_block", 51 | "character_height", 52 | "screen_width", 53 | "btn", 54 | "new_struct", 55 | "setup", 56 | "stub", 57 | "teardown", 58 | "after_each", 59 | "button_ids", 60 | "btn_states", 61 | "match", 62 | "log", 63 | "update_particles", 64 | "render_particles", 65 | "create_particle", 66 | "create_particle_set", 67 | "abs", 68 | "sspr", 69 | "particle_set", 70 | "chain_cube_particle_set", 71 | "particle_set_chain_cube", 72 | "particle_set_swap_left", 73 | "particle_set_swap_right", 74 | "particle_set_puff", 75 | "sin", 76 | "cos", 77 | "mfunc", 78 | "draw_rounded_box", 79 | "print_centered", 80 | "sset", 81 | "sub", 82 | "particle", 83 | "all_particles", 84 | "all_combo_bubbles", 85 | "combo_bubble", 86 | "create_qpu", 87 | "create_player", 88 | "tostr", 89 | "memset", 90 | "game_start_time", 91 | "t", 92 | "elapsed_time", 93 | "game_over_time", 94 | "block_class", 95 | "garbage_block_colors", 96 | "fillp", 97 | "spy", 98 | "demo_game", 99 | "init_ripple", 100 | "update_ripple", 101 | "render_ripple", 102 | "atan2", 103 | "sqrt", 104 | "ripple_speed", 105 | "render_plasma", 106 | "pset", 107 | "print_outlined", 108 | "ceil_rnd", 109 | "flip", 110 | "app_title", 111 | "circ", 112 | "_main_state", 113 | "pget", 114 | "game_class", 115 | "music", 116 | "effect_set", 117 | "reduction_rules" 118 | ], 119 | "Lua.workspace.library": ["${3rd}/lfs/library"], 120 | "Lua.workspace.checkThirdParty": false, 121 | "Lua.diagnostics.disable": [ 122 | "deprecated", 123 | "global-in-nil-env", 124 | "lowercase-global", 125 | "unbalanced-assignments" 126 | ] 127 | } 128 | -------------------------------------------------------------------------------- /test/chain_callback_utest.lua: -------------------------------------------------------------------------------- 1 | require("engine/test/bustedhelper") 2 | require("test/test_helper") 3 | require("lib/effects") 4 | require("lib/board") 5 | require("lib/player") 6 | require("lib/game") 7 | 8 | local match = require("luassert.match") 9 | 10 | describe('chain', function() 11 | local board 12 | local player 13 | 14 | before_each(function() 15 | stub(game_class, "chain_callback") 16 | board = board_class() 17 | board.attack_ion_target = { 85, 30 } 18 | player = player_class() 19 | end) 20 | 21 | it("chain コールバックが呼ばれる", function() 22 | -- T T T 23 | -- [X H] H X 24 | -- H X -----> H X -----> -----> T 25 | -- T T T T T T T T 26 | board:put(2, 4, block_class("t")) 27 | board:put(1, 3, block_class("x")) 28 | board:put(2, 3, block_class("h")) 29 | board:put(1, 2, block_class("h")) 30 | board:put(2, 2, block_class("x")) 31 | board:put(1, 1, block_class("t")) 32 | board:put(2, 1, block_class("t")) 33 | 34 | board:swap(1, 3) 35 | 36 | local chain_callback = assert.spy(game_class.chain_callback) 37 | 38 | wait_swap_to_finish(board) 39 | 40 | -- TODO: update 回数を式として書く 41 | for _i = 1, 200 do 42 | board:update(game_class, player) 43 | end 44 | 45 | chain_callback.was_called(1) 46 | chain_callback.was_called_with("2,3", 2, { board:screen_x(2), board:screen_y(2) }, match._, match._, match._) 47 | end) 48 | 49 | it("コールバックが呼ばれる", function() 50 | local chain_callback = assert.spy(game_class.chain_callback) 51 | 52 | -- G G G X Y Z 53 | -- H ---> ---> 54 | -- H Y Y X Z 55 | board:put(1, 3, garbage_block(3)) 56 | board:put(1, 2, block_class("h")) 57 | board:put(1, 1, block_class("h")) 58 | board:put(2, 1, block_class("y")) 59 | 60 | -- HH とおじゃまブロックがマッチ 61 | board:update() 62 | 63 | -- おじゃまブロックの一番左が分解 64 | for i = 1, block_class.block_match_animation_frame_count do 65 | board:update() 66 | end 67 | 68 | -- おじゃまブロックの真ん中が分解 69 | for i = 1, block_class.block_match_delay_per_block do 70 | board:update() 71 | end 72 | 73 | -- おじゃまブロックの一番右が分解 74 | for i = 1, block_class.block_match_delay_per_block do 75 | board:update() 76 | end 77 | 78 | -- 分解してできたブロックすべてのフリーズ解除 79 | for i = 1, block_class.block_match_delay_per_block do 80 | board:update() 81 | end 82 | board:update() 83 | 84 | -- 下の Y とマッチするように 85 | -- おじゃまブロック真ん中が分解してできたブロックを Y にする 86 | board:block_at(2, 3).type = "y" 87 | 88 | -- TODO: update 回数を式として書く 89 | for _i = 1, 200 do 90 | board:update(game_class, player) 91 | end 92 | 93 | chain_callback.was_called(1) 94 | chain_callback.was_called_with("1,2", 2, { board:screen_x(2), board:screen_y(2) }, match._, match._, match._) 95 | end) 96 | end) 97 | -------------------------------------------------------------------------------- /src/title/menu.lua: -------------------------------------------------------------------------------- 1 | require("lib/high_score") 2 | 3 | local menu_item = new_class() 4 | 5 | function menu_item._init(_ENV, _target_cart, _target_state, _sx, _sy, _width, _height, _cart_load_param, _label, 6 | _description, 7 | _high_score_slot) 8 | target_cart, target_state, sx, sy, width, height, cart_load_param, label, description, high_score = 9 | _target_cart, _target_state ~= "" and _target_state or nil, _sx, _sy, _width, _height, _cart_load_param, _label, 10 | _description, 11 | _high_score_slot and high_score_class(_high_score_slot):get() 12 | end 13 | 14 | menu_class = new_class() 15 | 16 | function menu_class._init(_ENV, items_string, previous_state) 17 | _items = {} 18 | for index, each in pairs(split(items_string, "|")) do 19 | _items[index] = menu_item(unpack_split(each)) 20 | end 21 | _active_item_index = 1 22 | _previous_state = previous_state 23 | end 24 | 25 | function menu_class.update(_ENV) 26 | if cart_to_load then 27 | if cart_load_delay > 0 then 28 | cart_load_delay = cart_load_delay - 1 29 | else 30 | jump(cart_to_load, nil, cart_load_param) 31 | end 32 | else 33 | if btnp(0) then -- left 34 | sfx(8) 35 | 36 | _active_item_index = max(_active_item_index - 1, 1) 37 | elseif btnp(1) then -- right 38 | sfx(8) 39 | 40 | _active_item_index = min(_active_item_index + 1, #_items) 41 | elseif btnp(5) then -- x 42 | sfx(15) 43 | 44 | local selected_menu_item = _items[_active_item_index] 45 | if selected_menu_item.target_state then 46 | stale = true 47 | return selected_menu_item.target_state 48 | else 49 | cart_to_load = selected_menu_item.target_cart 50 | cart_load_param = selected_menu_item.cart_load_param 51 | cart_load_delay = 30 52 | end 53 | elseif btnp(4) then -- c 54 | sfx(8) 55 | 56 | return _previous_state 57 | end 58 | end 59 | end 60 | 61 | function menu_class.draw(_ENV, left, top) 62 | local sx = left 63 | 64 | for i, each in pairs(_items) do 65 | if i == _active_item_index then 66 | print_centered(each.label, 62, top - 16, 10) 67 | print_centered(each.description, 62, top - 8, 7) 68 | 69 | draw_rounded_box(sx - 2, top - 2, sx + each.width + 1, top + each.height + 1, stale and 6 or 12) 70 | print_centered(each.high_score and 'hi score: ' .. tostr(each.high_score, 0x2), 62, top + 23, 7) 71 | 72 | if stale then 73 | pal(7, 6) 74 | end 75 | else 76 | pal(7, 13) 77 | end 78 | 79 | sspr(each.sx, each.sy, each.width, 16, sx, top) 80 | 81 | pal() 82 | 83 | sx = sx + each.width + 3 84 | end 85 | end 86 | 87 | function print_centered(text, center_x, center_y, col) 88 | if text then 89 | print(text, center_x - #text * 2 + 1, center_y - 2, col) 90 | end 91 | end 92 | -------------------------------------------------------------------------------- /src/main_rush.lua: -------------------------------------------------------------------------------- 1 | require("lib/helpers") 2 | require("lib/effects") 3 | require("lib/board") 4 | require("lib/player") 5 | require("lib/high_score") 6 | 7 | -- ハイスコア関係 8 | local high_score = high_score_class(0) 9 | local current_high_score 10 | 11 | local cursor = cursor_class() 12 | 13 | local board = board_class(cursor) 14 | board.attack_ion_target = { 85, 30 } 15 | 16 | local player = player_class() 17 | 18 | local game_class = require("rush/game") 19 | local game = game_class() 20 | local last_steps = -1 21 | 22 | function _init() 23 | current_high_score = high_score:get() 24 | 25 | player:init() 26 | board:init() 27 | board:put_random_blocks() 28 | cursor:init() 29 | 30 | game:init() 31 | game:add_player(player, board) 32 | end 33 | 34 | function _update60() 35 | game:update() 36 | 37 | if board.steps > last_steps then 38 | -- 5 ステップごとに 39 | -- * おじゃまブロックを降らせる 40 | -- * ブロックをせり上げるスピードを上げる 41 | if board.steps % 5 == 0 then 42 | if game.auto_raise_frame_count > 10 then 43 | game.auto_raise_frame_count = game.auto_raise_frame_count - 1 44 | end 45 | board:send_garbage(nil, 6, (board.steps + 5) / 5) 46 | end 47 | last_steps = board.steps 48 | end 49 | 50 | if game:is_game_over() then 51 | if t() - game.game_over_time > 2 then 52 | if high_score:put(player.score) then 53 | sfx(22) 54 | -- sash:create("high score!", 7, 8) 55 | sash:create("high score!,7,8") 56 | end 57 | 58 | board.show_gameover_menu = true 59 | if btnp(5) then -- x でリプレイ 60 | sfx(15) 61 | current_high_score = high_score:get() 62 | _init() 63 | elseif btnp(4) then -- z でタイトルへ戻る 64 | jump('quantattack_title') 65 | end 66 | end 67 | else 68 | if game.time_left <= 0 then 69 | board.timeup = true 70 | game.game_over_time = t() 71 | sfx(16) 72 | -- sash:create("time up!", 13, 7, function() 73 | sash:create("time up!,13,7", function() 74 | if high_score:put(player.score) then 75 | sfx(22) 76 | -- sash:create("high score!", 7, 8) 77 | sash:create("high score!,7,8") 78 | end 79 | end) 80 | end 81 | end 82 | 83 | sash:update_all() 84 | end 85 | 86 | function _draw() 87 | cls() 88 | 89 | game:render() 90 | 91 | local base_x = board.offset_x * 2 + board.width 92 | 93 | -- スコア表示 94 | print_outlined("score " .. tostr(player.score, 0x2), base_x, 16, 7, 0) 95 | print_outlined("hi-score " .. tostr(current_high_score, 0x2), base_x, 24, 7, 0) 96 | 97 | -- 残り時間表示 98 | print_outlined("time left", base_x, 44, 7, 0) 99 | print_outlined(game:time_left_string(), base_x, 52, 7, 0) 100 | 101 | if not game:is_game_over() then 102 | spr(99, base_x, 109) 103 | print_outlined("swap blocks", 81, 110, 7, 0) 104 | spr(112, base_x, 119) 105 | print_outlined("raise blocks", 81, 120, 7, 0) 106 | end 107 | 108 | sash:render_all() 109 | end 110 | -------------------------------------------------------------------------------- /scripts/install_data_cartridges_with_merging.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Install data cartridges to the default PICO-8 carts location, 4 | # merging data sections when needed to have 16 cartridges maximum. 5 | # This should be done as part of install cartridge with data, or after editing data. 6 | 7 | # Usage: install_data_cartridges_with_merging.sh config 8 | # config build config (e.g. 'debug' or 'release'. Default: 'debug') 9 | 10 | # Currently only supported on Linux 11 | 12 | # check that source and output paths have been provided 13 | if ! [[ $# == 1 ]] ; then 14 | echo "install_single_cartridge_with_data.sh takes 1 param, provided $#: 15 | \$1: config ('debug', 'release', etc. Default: 'debug')" 16 | 17 | exit 1 18 | fi 19 | 20 | # Configuration: paths 21 | data_path="$(dirname "$0")/../data" 22 | 23 | # Configuration: cartridge 24 | cartridge_stem="quantattack" 25 | version=`cat "$data_path/version.txt"` 26 | config="$1"; shift 27 | 28 | # recompute same install dirpath as used in install_single_cartridge.sh 29 | # (no need to mkdir -p "${install_dirpath}", it must have been created in said script) 30 | carts_dirpath="$HOME/.lexaloffle/pico-8/carts" 31 | install_dirpath="${carts_dirpath}/${cartridge_stem}/v${version}_${config}" 32 | 33 | # # Copy data cartridges 34 | # echo "Copying data cartridges data/data_*.p8 in ${install_dirpath} ..." 35 | # # trailing slash just to make sure we copy to a directory 36 | # cp data/data_*.p8 "${install_dirpath}/" 37 | 38 | # # Since data_start_cinematic.p8 is the 17th cartridge, it would go out of the 16-cartridge limit, 39 | # # so we must merge its content (__gfx__ section) into one of the existing cartridges for which 40 | # # we don't care about __gfx__ data at runtime, i.e. the stage tilemap cartridges (__gfx__ are only useful 41 | # # to visualize tiles during edit). So we rebuild data_stage1_00.p8 from itself (preserving __map__, we can throw 42 | # # away the __gff__ since flags are taken from builtin data) and data_start_cinematic.p8 (__gfx__). 43 | # # This means that the data_stage1_00.p8 located in install dirpath (under carts/) is not the same as the data_stage1_00.p8 44 | # # located in the project's data folder. We could rename it data_stage1_00_with_start_cinematic_gfx.p8 but that would 45 | # # mess up the dynamic region cartridge loading system. 46 | # # Note that we've already copied data_stage1_00.p8 with the command above, so this will overwrite it. 47 | # echo "Merging data_start_cinematic.p8 __gfx__ with data_stage1_00.p8 __map__ into ${install_dirpath}/data_stage1_00.p8 ..." 48 | # build_merged_data_cmd="p8tool build --gfx data/data_start_cinematic.p8 --map data/data_stage1_00.p8 \"${install_dirpath}/data_stage1_00.p8\"" 49 | # echo "> $build_merged_data_cmd" 50 | # bash -c "$build_merged_data_cmd" 51 | 52 | # # Now remove data_start_cinematic.p8 or it will be copied into ${cartridge_stem}_v${version}_${config}_cartridges folder 53 | # # on export, and zipped into the released archive for .p8 cartridges, being redundant with data_stage1_00.p8. 54 | # echo "Removing ${install_dirpath}/data_start_cinematic.p8 ..." 55 | # rm "${install_dirpath}/data_start_cinematic.p8" 56 | -------------------------------------------------------------------------------- /test/cursor_utest.lua: -------------------------------------------------------------------------------- 1 | require("engine/test/bustedhelper") 2 | require("lib/cursor") 3 | 4 | describe('cursor', function() 5 | local cursor 6 | 7 | before_each(function() 8 | cursor = cursor_class() 9 | end) 10 | 11 | describe('constructor', function() 12 | it('should be placed at x = 3, y = 6 by default', function() 13 | assert.are.equal(3, cursor.x) 14 | assert.are.equal(6, cursor.y) 15 | end) 16 | end) 17 | 18 | describe('move_left', function() 19 | it("should move left", function() 20 | cursor:move_left() 21 | 22 | assert.are.equal(2, cursor.x) 23 | assert.are.equal(6, cursor.y) 24 | end) 25 | 26 | it("should not move any further to the left when reaching the left edge", function() 27 | cursor:move_left() -- x = 2 28 | cursor:move_left() -- x = 1 29 | 30 | cursor:move_left() 31 | 32 | assert.are.equal(1, cursor.x) 33 | assert.are.equal(6, cursor.y) 34 | end) 35 | end) 36 | 37 | describe('move_right', function() 38 | it("should move right", function() 39 | cursor:move_right(6) 40 | 41 | assert.are.equal(4, cursor.x) 42 | assert.are.equal(6, cursor.y) 43 | end) 44 | 45 | it("should not move any further to the right when reaching the right edge", function() 46 | cursor:move_right(6) -- x = 4 47 | cursor:move_right(6) -- x = 5 48 | 49 | cursor:move_right(6) 50 | 51 | assert.are.equal(5, cursor.x) 52 | assert.are.equal(6, cursor.y) 53 | end) 54 | end) 55 | 56 | describe('move_up', function() 57 | it("should move up", function() 58 | cursor:move_up(12) 59 | 60 | assert.are.equal(3, cursor.x) 61 | assert.are.equal(7, cursor.y) 62 | end) 63 | 64 | it("should not move any upper when reaching the dead line", function() 65 | cursor:move_up(12) -- y = 7 66 | cursor:move_up(12) -- y = 8 67 | cursor:move_up(12) -- y = 9 68 | cursor:move_up(12) -- y = 10 69 | cursor:move_up(12) -- y = 11 70 | cursor:move_up(12) -- y = 12 71 | 72 | cursor:move_up(12) 73 | 74 | assert.are.equal(3, cursor.x) 75 | assert.are.equal(12, cursor.y) 76 | end) 77 | end) 78 | 79 | describe('move_down', function() 80 | it("should move down", function() 81 | cursor:move_down() 82 | 83 | assert.are.equal(3, cursor.x) 84 | assert.are.equal(5, cursor.y) 85 | end) 86 | 87 | it("should not move any further down when reaching the bottom", function() 88 | cursor:move_down() -- y = 5 89 | cursor:move_down() -- y = 4 90 | cursor:move_down() -- y = 3 91 | cursor:move_down() -- y = 2 92 | cursor:move_down() -- y = 1 93 | 94 | cursor:move_down() 95 | 96 | assert.are.equal(3, cursor.x) 97 | assert.are.equal(1, cursor.y) 98 | end) 99 | end) 100 | 101 | describe('update', function() 102 | it("should increment its tick", function() 103 | local tick = cursor._tick 104 | 105 | cursor:update() 106 | 107 | assert.are.equal(tick + 1, cursor._tick) 108 | end) 109 | end) 110 | end) 111 | -------------------------------------------------------------------------------- /src/lib/pending_garbage_blocks.lua: -------------------------------------------------------------------------------- 1 | ---@diagnostic disable: lowercase-global 2 | 3 | --- 待機中のおじゃまブロック 4 | pending_garbage_blocks_class = new_class() 5 | 6 | function pending_garbage_blocks_class._init(_ENV) 7 | all = {} 8 | end 9 | 10 | function pending_garbage_blocks_class.add_garbage(_ENV, span, height, chain_id) 11 | -- 同じ chain_id のおじゃまブロックをまとめる 12 | for _, each in pairs(all) do 13 | if each.chain_id == chain_id and each.span == 6 then 14 | if each.height <= height then 15 | -- 同じ chain_id でより低いおじゃまブロックがすでにプールに入っている場合、消す 16 | del(all, each) 17 | else 18 | -- 同じ chain_id でより高いおじゃまブロックがすでにプールに入っている場合、何もしない 19 | return 20 | end 21 | end 22 | end 23 | 24 | add(all, garbage_block(span, height, nil, chain_id, 60)) 25 | end 26 | 27 | --- おじゃまブロックの相殺 28 | function pending_garbage_blocks_class.offset(_ENV, chain_count) 29 | local offset_height = chain_count 30 | 31 | for _, each in pairs(all) do 32 | if each.span == 6 then 33 | if not each.tick_fall then 34 | if each.height > offset_height then 35 | each.height = each.height - offset_height 36 | break 37 | else 38 | offset_height = offset_height - each.height 39 | del(all, each) 40 | end 41 | end 42 | else 43 | offset_height = offset_height - 1 44 | del(all, each) 45 | end 46 | end 47 | 48 | return offset_height 49 | end 50 | 51 | function pending_garbage_blocks_class.update(_ENV, board) 52 | local first_garbage_block = all[1] 53 | 54 | if first_garbage_block then 55 | if first_garbage_block.tick_fall > 0 then 56 | if first_garbage_block.tick_fall < 30 then 57 | first_garbage_block.dy = ceil_rnd(2) - 1 58 | end 59 | first_garbage_block.tick_fall = first_garbage_block.tick_fall - 1 60 | else 61 | -- おじゃまブロックが幅いっぱいの場合、x = 1 62 | -- そうでない場合、 63 | -- x + span - 1 <= board.cols を満たす x をランダムに決める 64 | local x, y = first_garbage_block.span == board.cols and 65 | 1 or 66 | ceil_rnd(board.cols - first_garbage_block.span + 1), 67 | board.rows + 1 68 | 69 | if board:is_empty(x, y) then 70 | -- おじゃまブロックを落とす 71 | board:put(x, y, first_garbage_block) 72 | del(all, first_garbage_block) 73 | end 74 | end 75 | end 76 | end 77 | 78 | function pending_garbage_blocks_class.render(_ENV, board) 79 | for i, each in pairs(all) do 80 | if i < 6 then 81 | local x0, y0 = board.offset_x + 1 + (i - 1) * 9, each.dy 82 | 83 | if each.tick_fall then 84 | pal(7, each.inner_border_color) 85 | pal(6, each.inner_border_color) 86 | end 87 | 88 | if each.span < 6 then 89 | draw_rounded_box(x0, y0 + 4, x0 + 12, y0 + 9, 7, 7) 90 | draw_rounded_box(x0 + 1, y0 + 5, x0 + 11, y0 + 8, 0, 0) 91 | else 92 | draw_rounded_box(x0, y0 + 1, x0 + 12, y0 + 9, 7, 7) 93 | draw_rounded_box(x0 + 1, y0 + 2, x0 + 11, y0 + 8, 0, 0) 94 | 95 | cursor(x0 + 5, y0 + 3) 96 | print(each.height, 6) 97 | end 98 | 99 | pal() 100 | end 101 | end 102 | end 103 | -------------------------------------------------------------------------------- /src/lib/reduction_rules.lua: -------------------------------------------------------------------------------- 1 | ---@diagnostic disable: lowercase-global 2 | 3 | --- ブロックのマッチパターン 4 | reduction_rules = transform( 5 | transform( 6 | -- NOTE: ルールの行数を昇り順にならべておくことで、 7 | -- マッチする時に途中で探索を切り上げることができるようにする 8 | { 9 | h = 10 | "h\nh|,,\n,-1,|10&h\nx\nh|,,\n,-1,\n,-2,z|20&h\ny\nh|,,\n,-1,\n,-2,y|20&h\nz\nh|,,\n,-1,\n,-2,x|20&h\nswap,swap\n?,h|,,\ntrue,-2,|50&h\nx\nswap,swap\n?,h|,,\n,-1,z\ntrue,-3,|60&h\ny\nswap,swap\n?,h|,,\n,-1,y\ntrue,-3,|60&h\nswap,swap\n?,x\n?,h|,,z\ntrue,-2,\ntrue,-3,|60&h\nswap,swap\n?,y\n?,h|,,y\ntrue,-2,\ntrue,-3,|60&h\nz\nswap,swap\n?,h|,,\n,-1,x\ntrue,-3,|60&h\nswap,swap\n?,z\n?,h|,,x\ntrue,-2,\ntrue,-3,|60", 11 | x = 12 | "x\nx|,,\n,-1,|10&x,x\ncontrol,cnot_x\nx|,,\ntrue,,\n,-2,|90&x,x\ncnot_x,control\nx|,,\ntrue,,\n,-2,|90&x\ncnot_x,control\nx|,,\n,-2,|80&x\nswap,swap\n?,x|,,\ntrue,-2,|50", 13 | y = 14 | "y\ny|,,\n,-1,|10&y\nswap,swap\n?,y|,,\ntrue,-2,|50&y,x\ncontrol,cnot_x\ny|,,\ntrue,,\n,-2,|90&y,z\ncnot_x,control\ny|,,\ntrue,,\n,-2,|90", 15 | z = 16 | "z\nz|,,\n,-1,|10&z,z\ncontrol,cnot_x\n?,z|,,\ntrue,,\ntrue,-2,|90&z\ncontrol,cnot_x\nz|,,\n,-2,|80&z\nswap,swap\n?,z|,,\ntrue,-2,|50", 17 | s = 18 | "s\ns|,,\n,-1,z|10&s\nx\ns|,,\n,-1,\n,-2,x|20&s\ny\ns|,,\n,-1,\n,-2,y|20&s\nz\ns|,,\n,-1,\n,-2,|20&s\nswap,swap\n?,s|,,z\ntrue,-2,|50&s\nx\nswap,swap\n?,s|,,\n,-1,x\ntrue,-3,|60&s\ny\nswap,swap\n?,s|,,\n,-1,y\ntrue,-3,|60&s\nz\nswap,swap\n?,s|,,\n,-1,\ntrue,-3,|60&s\nswap,swap\n?,x\n?,s|,,x\ntrue,-2,\ntrue,-3,|60&s\nswap,swap\n?,y\n?,s|,,y\ntrue,-2,\ntrue,-3,|60&s\nswap,swap\n?,z\n?,s|,,\ntrue,-2,\ntrue,-3,|60", 19 | t = 20 | "t\nt|,,\n,-1,s|10&t\ns\nt|,,\n,-1,\n,-2,z|20&t\nswap,swap\n?,t|,,s\ntrue,-2,|50&t\nz\ns\nt|,,\n,-1,\n,-2,\n,-3,|30&t\ns\nz\nt|,,\n,-1,\n,-2,\n,-3,|30&t\ns\nswap,swap\n?,t|,,\n,-1,z\ntrue,-3,|60&t\nswap,swap\n?,s\n?,t|,,z\ntrue,-2,\ntrue,-3,|60&t\nswap,swap\n?,z\n?,s\n?,t|,,\ntrue,-2,\ntrue,-3,\ntrue,-4,|70&t\nswap,swap\n?,s\n?,z\n?,t|,,\ntrue,-2,\ntrue,-3,\ntrue,-4,|70&t\nz\nswap,swap\n?,s\n?,t|,,\n,-1,\ntrue,-3,\ntrue,-4,|70&t\ns\nswap,swap\n?,z\n?,t|,,\n,-1,\ntrue,-3,\ntrue,-4,|70&t\nz\ns\nswap,swap\n?,t|,,\n,-1,\n,-2,\ntrue,-4,|70&t\ns\nz\nswap,swap\n?,t|,,\n,-1,\n,-2,\ntrue,-4,|70", 21 | control = "control,cnot_x\nswap,swap\ncnot_x,control|,,\ntrue,,\n,-2,\ntrue,-2,|200", 22 | cnot_x = 23 | "cnot_x,control\ncnot_x,control|,,\ntrue,,\n,-1,\ntrue,-1,|40&cnot_x,control\ncontrol,cnot_x\ncnot_x,control|,,\ntrue,,\n,-1,\ntrue,-1,\n,-2,swap\ntrue,-2,swap|100", 24 | swap = "swap,swap\nswap,swap|,,\ntrue,,\n,-1,\ntrue,-1,|300" 25 | }, 26 | function(rule_string) return split(rule_string, "&") end), 27 | function(gate_rules) 28 | return transform( 29 | gate_rules, 30 | function(each) 31 | local pattern, reduce_to, score = unpack(split(each, "|")) 32 | 33 | return { 34 | transform(split(pattern, "\n"), split), 35 | transform(split(reduce_to, "\n"), function(to) 36 | local attrs = split(to) 37 | return { 38 | dx = attrs[1] ~= "", 39 | dy = attrs[2] == "" and nil or tonum(attrs[2]), 40 | block_type = attrs[3] == "" and 'i' or attrs[3] 41 | } 42 | end), 43 | tonum(score) 44 | } 45 | end 46 | ) 47 | end 48 | ) 49 | -------------------------------------------------------------------------------- /test/combo_callback_utest.lua: -------------------------------------------------------------------------------- 1 | require("engine/test/bustedhelper") 2 | require("test/test_helper") 3 | require("lib/helpers") 4 | require("lib/game") 5 | require("lib/effects") 6 | require("lib/board") 7 | 8 | describe('コンボ (同時消し) のコールバック', function() 9 | local board 10 | local match = require("luassert.match") 11 | local _ = match._ 12 | 13 | before_each(function() 14 | board = board_class() 15 | stub(game_class, "combo_callback") 16 | end) 17 | 18 | it("4-コンボ発生でコールバックが呼ばれる", function() 19 | -- [X H] H X <- 4-combo 20 | -- H X -----> H X 21 | board:put(1, 2, block_class("x")) 22 | board:put(1, 1, block_class("h")) 23 | board:put(2, 2, block_class("h")) 24 | board:put(2, 1, block_class("x")) 25 | 26 | board:swap(1, 1) 27 | wait_swap_to_finish(board) 28 | 29 | local combo_callback = assert.spy(game_class.combo_callback) 30 | board:update(game_class) 31 | 32 | combo_callback.was_called(1) 33 | combo_callback.was_called_with(4, { board:screen_x(2), board:screen_y(2) }, _, _, _) 34 | end) 35 | 36 | it("5-コンボ発生でコールバックが呼ばれる", function() 37 | -- S S 38 | -- [Z H] H Z 39 | -- H S -----> H S 40 | board:put(2, 3, block_class("s")) 41 | board:put(1, 2, block_class("z")) 42 | board:put(2, 2, block_class("h")) 43 | board:put(1, 1, block_class("h")) 44 | board:put(2, 1, block_class("s")) 45 | 46 | board:swap(1, 2) 47 | wait_swap_to_finish(board) 48 | 49 | local combo_callback = assert.spy(game_class.combo_callback) 50 | board:update(game_class) 51 | 52 | combo_callback.was_called(1) 53 | combo_callback.was_called_with(5, { board:screen_x(1), board:screen_y(2) }, _, _, _) 54 | end) 55 | 56 | it("6-コンボ発生でコールバックが呼ばれる", function() 57 | -- [S H] H S <- 6-combo 58 | -- X Z Z Z 59 | -- H S -----> H S 60 | board:put(1, 3, block_class("s")) 61 | board:put(2, 3, block_class("h")) 62 | board:put(1, 2, block_class("x")) 63 | board:put(2, 2, block_class("z")) 64 | board:put(1, 1, block_class("h")) 65 | board:put(2, 1, block_class("s")) 66 | 67 | board:swap(1, 3) 68 | wait_swap_to_finish(board) 69 | 70 | local combo_callback = assert.spy(game_class.combo_callback) 71 | board:update(game_class) 72 | 73 | combo_callback.was_called(1) 74 | combo_callback.was_called_with(6, { board:screen_x(2), board:screen_y(3) }, _, _, _) 75 | end) 76 | 77 | it("7-コンボ発生でコールバックが呼ばれる", function() 78 | -- T T 79 | -- H Z H Z 80 | -- X S X S 81 | -- [T H] -----> H T 82 | board:put(2, 4, block_class("t")) 83 | board:put(1, 3, block_class("h")) 84 | board:put(2, 3, block_class("z")) 85 | board:put(1, 2, block_class("x")) 86 | board:put(2, 2, block_class("s")) 87 | board:put(1, 1, block_class("t")) 88 | board:put(2, 1, block_class("h")) 89 | 90 | board:swap(1, 1) 91 | wait_swap_to_finish(board) 92 | 93 | local combo_callback = assert.spy(game_class.combo_callback) 94 | board:update(game_class) 95 | 96 | combo_callback.was_called(1) 97 | combo_callback.was_called_with(7, { board:screen_x(1), board:screen_y(3) }, _, _, _) 98 | end) 99 | end) 100 | -------------------------------------------------------------------------------- /src/title/game.lua: -------------------------------------------------------------------------------- 1 | ---@diagnostic disable: global-in-nil-env, lowercase-global 2 | 3 | title_logo_bounce_speed, title_logo_bounce_screen_dy = 0, 0 4 | 5 | -- タイトルロゴを跳ねさせる 6 | local function bounce_title_logo() 7 | title_logo_bounce_screen_dy, title_logo_bounce_speed = 0, -5 8 | end 9 | 10 | function update_title_logo_bounce() 11 | if title_logo_bounce_speed ~= 0 then 12 | title_logo_bounce_speed = title_logo_bounce_speed + 0.9 13 | title_logo_bounce_screen_dy = title_logo_bounce_screen_dy + title_logo_bounce_speed 14 | 15 | if title_logo_bounce_screen_dy > 0 then 16 | title_logo_bounce_screen_dy, title_logo_bounce_speed = 0, -title_logo_bounce_speed 17 | end 18 | end 19 | end 20 | 21 | local attack_cube_callback = function(target) 22 | bounce_title_logo() 23 | sfx(19) 24 | particles:create( 25 | target, 26 | "5,5,9,7,,,-0.03,-0.03,20|5,5,9,7,,,-0.03,-0.03,20|4,4,9,7,,,-0.03,-0.03,20|4,4,2,5,,,-0.03,-0.03,20|4,4,6,7,,,-0.03,-0.03,20|2,2,9,7,,,-0.03,-0.03,20|2,2,9,7,,,-0.03,-0.03,20|2,2,6,5,,,-0.03,-0.03,20|2,2,6,5,,,-0.03,-0.03,20|0,0,2,5,,,-0.03,-0.03,20" 27 | ) 28 | end 29 | 30 | function game() 31 | return setmetatable({ 32 | reduce_callback = function(_score, _player) 33 | -- NOP 34 | end, 35 | 36 | combo_callback = function(combo_count, coord, _player, board, _other_board) 37 | bubbles:create("combo", combo_count, coord) 38 | ions:create(coord, { 64, 36 }, attack_cube_callback) 39 | end, 40 | 41 | block_offset_callback = function(chain_count) 42 | return chain_count 43 | end, 44 | 45 | chain_callback = function(_chain_id, chain_count, coord) 46 | if chain_count > 1 then 47 | bubbles:create("chain", chain_count, coord) 48 | ions:create(coord, { 64, 36 }, attack_cube_callback) 49 | end 50 | end, 51 | 52 | init = function(_ENV) 53 | all_players_info = {} 54 | end, 55 | 56 | add_player = function(_ENV, player, board) 57 | add(all_players_info, { 58 | player = player, 59 | board = board, 60 | tick = 0 61 | }) 62 | end, 63 | 64 | update = function(_ENV) 65 | for _, each in pairs(all_players_info) do 66 | local player, board, cursor = each.player, each.board, each.board.cursor 67 | 68 | player:update(board) 69 | 70 | if player.left then 71 | cursor:move_left() 72 | end 73 | if player.right then 74 | cursor:move_right(board.cols) 75 | end 76 | if player.up then 77 | cursor:move_up(board.rows) 78 | end 79 | if player.down then 80 | cursor:move_down() 81 | end 82 | if player.x then 83 | board:swap(cursor.x, cursor.y) 84 | end 85 | if player.o then 86 | _raise(_ENV, each) 87 | end 88 | 89 | board:update(_ENV, player) 90 | cursor:update() 91 | end 92 | 93 | particles:update_all() 94 | bubbles:update_all() 95 | ions:update_all() 96 | end, 97 | 98 | render = function(_ENV) 99 | for _, each in pairs(all_players_info) do 100 | each.board:render() 101 | end 102 | 103 | particles:render_all() 104 | bubbles:render_all() 105 | ions:render_all() 106 | end, 107 | 108 | -- ブロックをせりあげる 109 | _raise = function(_ENV, player_info) 110 | local board = player_info.board 111 | 112 | board.raised_dots = board.raised_dots + 1 113 | 114 | if board.raised_dots == 8 then 115 | board.raised_dots = 0 116 | board:insert_blocks_at_bottom() 117 | board.cursor:move_up(board.rows) 118 | end 119 | end 120 | }, { __index = _ENV }) 121 | end 122 | -------------------------------------------------------------------------------- /src/tutorial/dtb.lua: -------------------------------------------------------------------------------- 1 | ---@diagnostic disable: lowercase-global 2 | 3 | require("lib/helpers") 4 | 5 | -- call this before you start using dtb. 6 | -- optional parameter is the number of lines that are displayed. default is 3. 7 | function dtb_init(numlines) 8 | dtb_queu = {} 9 | dtb_queuf = {} 10 | dtb_numlines = 3 11 | if numlines then 12 | dtb_numlines = numlines 13 | end 14 | _dtb_clean() 15 | end 16 | 17 | -- this will add a piece of text to the queu. the queu is processed automatically. 18 | function dtb_disp(txt, callback) 19 | local lines = {} 20 | local currline = "" 21 | local curword = "" 22 | local curchar = "" 23 | local upt = function() 24 | if #curword + #currline > 27 then 25 | add(lines, currline) 26 | currline = "" 27 | end 28 | currline = currline .. curword 29 | curword = "" 30 | end 31 | for i = 1, #txt do 32 | curchar = sub(txt, i, i) 33 | curword = curword .. curchar 34 | if curchar == " " then 35 | upt() 36 | elseif #curword > 26 then 37 | curword = curword .. "-" 38 | upt() 39 | end 40 | end 41 | upt() 42 | if currline ~= "" then 43 | add(lines, currline) 44 | end 45 | add(dtb_queu, lines) 46 | if callback == nil then 47 | callback = 0 48 | end 49 | add(dtb_queuf, callback) 50 | end 51 | 52 | -- functions with an underscore prefix are ment for internal use, don't worry about them. 53 | function _dtb_clean() 54 | dtb_dislines = {} 55 | for i = 1, dtb_numlines do 56 | add(dtb_dislines, "") 57 | end 58 | dtb_curline = 0 59 | dtb_ltime = 0 60 | end 61 | 62 | function _dtb_nextline() 63 | dtb_curline = dtb_curline + 1 64 | for i = 1, #dtb_dislines - 1 do 65 | dtb_dislines[i] = dtb_dislines[i + 1] 66 | end 67 | dtb_dislines[#dtb_dislines] = "" 68 | sfx(8) 69 | end 70 | 71 | function _dtb_nexttext() 72 | if dtb_queuf[1] ~= 0 then 73 | dtb_queuf[1]() 74 | end 75 | del(dtb_queuf, dtb_queuf[1]) 76 | del(dtb_queu, dtb_queu[1]) 77 | _dtb_clean() 78 | sfx(8) 79 | end 80 | 81 | -- make sure that this function is called each update. 82 | function dtb_update() 83 | if #dtb_queu > 0 then 84 | if dtb_curline == 0 then 85 | dtb_curline = 1 86 | end 87 | local dislineslength = #dtb_dislines 88 | local curlines = dtb_queu[1] 89 | local curlinelength = #dtb_dislines[dislineslength] 90 | local complete = curlinelength >= #curlines[dtb_curline] 91 | if complete and dtb_curline >= #curlines then 92 | if btnp(5) then 93 | _dtb_nexttext() 94 | return 95 | end 96 | elseif dtb_curline > 0 then 97 | dtb_ltime = dtb_ltime - 1 98 | if not complete then 99 | if dtb_ltime <= 0 then 100 | local curchari = curlinelength + 1 101 | local curchar = sub(curlines[dtb_curline], curchari, curchari) 102 | dtb_ltime = 1 103 | if curchar ~= " " then 104 | sfx(24) 105 | end 106 | if curchar == "." then 107 | dtb_ltime = 6 108 | end 109 | dtb_dislines[dislineslength] = dtb_dislines[dislineslength] .. curchar 110 | end 111 | if btnp(5) then 112 | dtb_dislines[dislineslength] = curlines[dtb_curline] 113 | end 114 | else 115 | if btnp(5) then 116 | _dtb_nextline() 117 | end 118 | end 119 | end 120 | end 121 | end 122 | 123 | function dtb_draw() 124 | if #dtb_queu > 0 then 125 | local dislineslength = #dtb_dislines 126 | local offset = 0 127 | if dtb_curline < dislineslength then 128 | offset = dislineslength - dtb_curline 129 | end 130 | 131 | -- 「次へ」ボタン 132 | if dtb_curline > 0 and #dtb_dislines[#dtb_dislines] == #dtb_queu[1][dtb_curline] then 133 | spr(99, 113, 21) 134 | end 135 | 136 | for i = 1, dislineslength do 137 | print_outlined(dtb_dislines[i], 8, i * 8 + 24 - (dislineslength + offset) * 8, 7, 0) 138 | end 139 | end 140 | end 141 | -------------------------------------------------------------------------------- /test/block_utest.lua: -------------------------------------------------------------------------------- 1 | require("engine/test/bustedhelper") 2 | require("test/test_helper") 3 | require("lib/board") 4 | 5 | describe('block', function() 6 | describe('type', function() 7 | it("type を指定", function() 8 | local g = block_class("i") 9 | 10 | assert.are.equal("i", g.type) 11 | end) 12 | 13 | it("すべての type のブロックを作る", function() 14 | assert.has_no.errors(function() block_class("i") end) 15 | assert.has_no.errors(function() block_class("h") end) 16 | assert.has_no.errors(function() block_class("x") end) 17 | assert.has_no.errors(function() block_class("y") end) 18 | assert.has_no.errors(function() block_class("z") end) 19 | assert.has_no.errors(function() block_class("s") end) 20 | assert.has_no.errors(function() block_class("t") end) 21 | assert.has_no.errors(function() block_class("control") end) 22 | assert.has_no.errors(function() block_class("cnot_x") end) 23 | assert.has_no.errors(function() block_class("swap") end) 24 | assert.has_no.errors(function() block_class("g") end) 25 | assert.has_no.errors(function() block_class("?") end) 26 | end) 27 | end) 28 | 29 | describe('span', function() 30 | it("幅はデフォルトで 1", function() 31 | assert.are.equal(1, block_class("i").span) 32 | end) 33 | 34 | it("幅 (span) を指定", function() 35 | assert.are.equal(3, block_class("g", 3).span) 36 | end) 37 | end) 38 | 39 | describe('height', function() 40 | it("高さはデフォルトで 1", function() 41 | assert.are.equal(1, block_class("i").height) 42 | end) 43 | 44 | it("高さ (height) を指定", function() 45 | assert.are.equal(4, block_class("g", 1, 4).height) 46 | end) 47 | end) 48 | 49 | describe('render', function() 50 | it('should render without errors', function() 51 | local board = board_class() 52 | local block = block_class("h") 53 | 54 | board:put(1, 1, block) 55 | 56 | assert.has_no.errors(function() block:render(board:screen_x(1), board:screen_y(1)) end) 57 | end) 58 | end) 59 | 60 | describe('swap_with("left")', function() 61 | local block 62 | 63 | before_each(function() 64 | local board = board_class() 65 | block = block_class("h") 66 | 67 | board:put(2, 1, block) 68 | end) 69 | 70 | it('should swap with the left block without errors', function() 71 | assert.has_no.errors(function() block:swap_with("left") end) 72 | end) 73 | 74 | it('should transition its state to swapping', function() 75 | block:swap_with("left") 76 | 77 | assert.is_true(block.state == "swap") 78 | end) 79 | end) 80 | 81 | describe('swap_with("right")', function() 82 | local block 83 | 84 | before_each(function() 85 | local board = board_class() 86 | block = block_class("h") 87 | 88 | board:put(1, 1, block) 89 | end) 90 | 91 | it('should swap with the right block without errors', function() 92 | assert.has_no.errors(function() block:swap_with("right") end) 93 | end) 94 | 95 | it('should transition its state to swapping', function() 96 | block:swap_with("right") 97 | 98 | assert.is_true(block.state == "swap") 99 | end) 100 | end) 101 | 102 | describe('drop', function() 103 | local block 104 | 105 | before_each(function() 106 | local board = board_class() 107 | block = block_class("h") 108 | 109 | board:put(1, 1, block) 110 | end) 111 | 112 | it('should drop the block without errors', function() 113 | assert.has_no.errors(function() block:fall() end) 114 | end) 115 | 116 | it('should transition its state to fall', function() 117 | block:fall() 118 | 119 | assert.is_true(block.state == "fall") 120 | end) 121 | end) 122 | 123 | it('should replace with other block', function() 124 | local board = board_class() 125 | local block = block_class("h") 126 | local other_block = block_class("x") 127 | board:put(1, 1, block) 128 | 129 | block:replace_with(other_block) 130 | 131 | assert.are.equal(other_block, block.new_block) 132 | end) 133 | end) 134 | -------------------------------------------------------------------------------- /src/main_title.lua: -------------------------------------------------------------------------------- 1 | require("lib/helpers") 2 | require("lib/effects") 3 | require("lib/board") 4 | require("lib/qpu") 5 | require("title/game") 6 | require("title/plasma") 7 | require("title/menu") 8 | 9 | demo_game = game() 10 | 11 | local main_menu = menu_class( 12 | "quantattack_tutorial,,32,48,16,16,,tutorial,learn how to play|quantattack_endless,,64,48,16,16,,endless,play as long as you can, 1|quantattack_rush,,48,48,16,16,,rush,play for 2 minutes,0|,:level_menu,80,48,16,16,,vs qpu,defeat the qpu|quantattack_qpu_vs_qpu,,96,48,16,16,,qpu vs qpu,watch qpu vs qpu|quantattack_vs_human,,112,48,16,16,,vs human,player1 vs player2" 13 | , 14 | ":demo" 15 | ) 16 | local level_menu = menu_class( 17 | "quantattack_vs_qpu,,48,80,19,7,3|quantattack_vs_qpu,,72,80,27,7,2|quantattack_vs_qpu,,104,80,19,7,1", 18 | ":main_menu" 19 | ) 20 | 21 | -- :logo_slidein QuantumAttack のロゴ slide-in アニメーション 22 | -- :board_fadein ボードの fade-in アニメーション 23 | -- :demo デモプレイ 24 | -- :main_menu メニューを表示した状態 25 | -- :level_menu QPU のレベル選択 26 | local title_state = ":logo_slidein" 27 | local tick = 0 28 | 29 | function _init() 30 | local qpu_board = board_class(cursor_class(), 0, 16) 31 | 32 | qpu_board:put_random_blocks() 33 | 34 | qpu_board.show_top_line = false 35 | 36 | demo_game:init() 37 | demo_game:add_player(qpu_class(qpu_board, 1), qpu_board) 38 | 39 | music(32) 40 | end 41 | 42 | function _update60() 43 | if title_state == ":board_fadein" then 44 | demo_game:update() 45 | elseif title_state == ":demo" then 46 | demo_game:update() 47 | update_title_logo_bounce() 48 | 49 | if btnp(5) then -- x でタイトルへ進む 50 | sfx(15) 51 | title_state = ":main_menu" 52 | end 53 | elseif title_state == ":main_menu" then 54 | if main_menu._active_item_index == 4 then 55 | level_menu.stale = true 56 | end 57 | 58 | main_menu.stale = false 59 | title_state = main_menu:update() or title_state 60 | elseif title_state == ":level_menu" then 61 | level_menu.stale = false 62 | title_state = level_menu:update() or title_state 63 | end 64 | 65 | tick = tick + 1 66 | end 67 | 68 | function _draw() 69 | cls() 70 | 71 | render_plasma() 72 | 73 | if title_state == ":logo_slidein" then 74 | sspr(unpack_split("0,64,128,16,0," .. tick)) 75 | 76 | if tick > 24 then 77 | title_state = ":board_fadein" 78 | end 79 | elseif title_state == ":board_fadein" then 80 | sspr(unpack_split("0,64,128,16,0,24")) 81 | 82 | if tick <= 90 then 83 | fadein((tick - 26) / 3) 84 | end 85 | demo_game:render() 86 | pal() 87 | 88 | if tick > 90 then 89 | title_state = ":demo" 90 | end 91 | else 92 | sspr(0, 64, 128, 16, 0, 24 + title_logo_bounce_screen_dy) 93 | 94 | demo_game:render() 95 | 96 | if title_state == ":demo" then 97 | -- X start を表示 98 | if tick % 60 < 30 then 99 | print_outlined("x start", 50, 50, 1, 10) 100 | end 101 | else -- ":main_menu" or ":level_menu" 102 | -- メニューのウィンドウを表示 103 | draw_rounded_box(unpack_split("1,46,125,108,0,0")) -- ふちどり 104 | draw_rounded_box(unpack_split("2,47,124,107,12,12")) -- 枠線 105 | draw_rounded_box(unpack_split("4,49,122,105,1,1")) -- 本体 106 | 107 | -- メニューを表示 108 | main_menu:draw(8, 72) 109 | 110 | -- レベル選択メニューを表示 111 | if main_menu._active_item_index == 4 or title_state == ":level_menu" then 112 | level_menu:draw(27, 93) 113 | end 114 | end 115 | end 116 | end 117 | 118 | function fadein(i) 119 | local index = flr(15 - i) 120 | 121 | for c = 0, 15 do 122 | if index < 1 then 123 | pal(c, c) 124 | else 125 | pal(c, 126 | split(split("0,0,0,0,0,0,0,0,0,0,0,0,0,0,0|1,1,129,129,129,129,129,129,129,129,0,0,0,0,0|2,2,2,130,130,130,130,130,128,128,128,128,128,0,0|3,3,3,131,131,131,131,129,129,129,129,129,0,0,0|4,4,132,132,132,132,132,132,130,128,128,128,128,0,0|5,5,133,133,133,133,130,130,128,128,128,128,128,0,0|6,6,134,13,13,13,141,5,5,5,133,130,128,128,0|7,6,6,6,134,134,134,134,5,5,5,133,130,128,0|8,8,136,136,136,136,132,132,132,130,128,128,128,128,0|9,9,9,4,4,4,4,132,132,132,128,128,128,128,0|10,10,138,138,138,4,4,4,132,132,133,128,128,128,0|11,139,139,139,139,3,3,3,3,129,129,129,0,0,0|12,12,12,140,140,140,140,131,131,131,1,129,129,129,0|13,13,141,141,5,5,5,133,133,130,129,129,128,128,0|14,14,14,134,134,141,141,2,2,133,130,130,128,128,0|15,143,143,134,134,134,134,5,5,5,133,133,128,128,0" 127 | , "|")[c + 1])[index]) 128 | end 129 | end 130 | end 131 | -------------------------------------------------------------------------------- /scripts/build_single_cartridge.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Build a specific cartridge for the game 4 | # It relies on pico-boots/scripts/build_cartridge.sh 5 | # It also defines game information and defined symbols per config. 6 | 7 | # Configuration: paths 8 | picoboots_scripts_path="$(dirname "$0")/../pico-boots/scripts" 9 | game_src_path="$(dirname "$0")/../src" 10 | data_path="$(dirname "$0")/../data" 11 | build_dir_path="$(dirname "$0")/../build" 12 | 13 | # Configuration: cartridge 14 | version=`cat "$data_path/version.txt"` 15 | author="yasuhito" 16 | cartridge_stem="quantattack" 17 | title="quantattack v$version" 18 | 19 | help() { 20 | echo "Build a PICO-8 cartridge with the passed config." 21 | usage 22 | } 23 | 24 | usage() { 25 | echo "Usage: build_single_cartridge.sh CARTRIDGE_SUFFIX [CONFIG] [OPTIONS] 26 | ARGUMENTS 27 | CARTRIDGE_SUFFIX Cartridge to build for the multi-cartridge game 28 | See data/cartridges.txt for the list of cartridge names 29 | A symbol equal to the cartridge suffix is always added 30 | to the config symbols. 31 | CONFIG Build config. Determines defined preprocess symbols. 32 | (default: 'debug') 33 | -h, --help Show this help message 34 | " 35 | } 36 | 37 | # Default parameters 38 | config='debug' 39 | 40 | # Read arguments 41 | # https://stackoverflow.com/questions/192249/how-do-i-parse-command-line-arguments-in-bash 42 | while [[ $# -gt 0 ]]; do 43 | case $1 in 44 | -h | --help ) 45 | help 46 | exit 0 47 | ;; 48 | -* ) # unknown option 49 | echo "Unknown option: '$1'" 50 | usage 51 | exit 1 52 | ;; 53 | * ) # store positional argument for later 54 | positional_args+=("$1") 55 | shift # past argument 56 | ;; 57 | esac 58 | done 59 | 60 | if ! [[ ${#positional_args[@]} -ge 1 && ${#positional_args[@]} -le 2 ]]; then 61 | echo "Wrong number of positional arguments: found ${#positional_args[@]}, expected 1 or 2." 62 | echo "Passed positional arguments: ${positional_args[@]}" 63 | usage 64 | exit 1 65 | fi 66 | 67 | if [[ ${#positional_args[@]} -ge 1 ]]; then 68 | cartridge_suffix="${positional_args[0]}" 69 | fi 70 | 71 | if [[ ${#positional_args[@]} -ge 2 ]]; then 72 | config="${positional_args[1]}" 73 | fi 74 | 75 | # Define build output folder from config 76 | # (to simplify cartridge loading, cartridge files are always named the same, 77 | # so we can only distinguish builds by their folder names) 78 | build_output_path="${build_dir_path}/v${version}_${config}" 79 | 80 | # Define symbols from config 81 | symbols='' 82 | 83 | if [[ $config == 'debug' ]]; then 84 | symbols='tostring,dump' 85 | elif [[ $config == 'release' ]]; then 86 | # usually release has no symbols except those that help making the code more compact 87 | # in this game project we define 'release' as a special symbol for that 88 | # most fo the time, we could replace `#if release` with 89 | # `#if debug_option1 || debug_option2 || debug_option3 ` but the problem is that 90 | # 2+ OR statements syntax is not supported by preprocess.py yet 91 | symbols='release' 92 | fi 93 | 94 | # we always add a symbol for the cartridge suffix in case 95 | # we want to customize the build of the same script 96 | # depending on the cartridge it is built into 97 | if [[ -n "$symbols" ]]; then 98 | # there was at least one symbol before, so add comma separator 99 | symbols+="," 100 | fi 101 | symbols+="$cartridge_suffix" 102 | 103 | builtin_data_suffix="$cartridge_suffix" 104 | data_filebasename="builtin_data_${builtin_data_suffix}" 105 | 106 | # Build cartridges without version nor config appended to name 107 | # so we can use PICO-8 load() with a cartridge file name 108 | # independent from the version and config 109 | 110 | # Build cartridge 111 | # See data/cartridges.txt for the list of cartridge names 112 | # metadata really counts for the entry cartridge (titlemenu) 113 | "$picoboots_scripts_path/build_cartridge.sh" \ 114 | "$game_src_path" \ 115 | main_${cartridge_suffix}.lua \ 116 | -d "${data_path}/${data_filebasename}.p8" \ 117 | -M "$data_path/metadata.p8" \ 118 | -a "$author" -t "$title (${cartridge_suffix})" \ 119 | -p "$build_output_path" \ 120 | -o "${cartridge_stem}_${cartridge_suffix}" \ 121 | -s "$symbols" \ 122 | --minify-level 1 123 | 124 | if [[ $? -ne 0 ]]; then 125 | echo "" 126 | echo "Build failed, STOP." 127 | exit 1 128 | fi 129 | -------------------------------------------------------------------------------- /test/block/garbage_block_utest.lua: -------------------------------------------------------------------------------- 1 | require("engine/test/bustedhelper") 2 | require("lib/helpers") 3 | require("lib/board") 4 | 5 | describe('garbage_block', function() 6 | describe('span', function() 7 | it("幅はデフォルトで 6", function() 8 | local garbage = garbage_block() 9 | 10 | assert.are.equal(6, garbage.span) 11 | end) 12 | 13 | it("幅 (span) を指定して生成", function() 14 | local garbage = garbage_block(3) 15 | 16 | assert.are.equal(3, garbage.span) 17 | end) 18 | 19 | it("幅を 2 以下に指定するとエラー", function() 20 | assert.has.errors(function() garbage_block(2) end) 21 | end) 22 | 23 | it("幅を 7 以上に指定するとエラー", function() 24 | assert.has.errors(function() garbage_block(7) end) 25 | end) 26 | end) 27 | 28 | describe('height', function() 29 | it("高さはデフォルトで 1", function() 30 | local garbage = garbage_block(3) 31 | 32 | assert.are.equal(1, garbage.height) 33 | end) 34 | 35 | it("高さ (height) を指定して生成", function() 36 | local garbage = garbage_block(3, 4) 37 | 38 | assert.are.equal(4, garbage.height) 39 | end) 40 | end) 41 | 42 | describe('body_color', function() 43 | it("色を指定して生成 (2, 3, 4)", function() 44 | local garbage 45 | 46 | garbage = garbage_block(6, 1, 2) 47 | assert.are.equal(2, garbage.body_color) 48 | 49 | garbage = garbage_block(6, 1, 3) 50 | assert.are.equal(3, garbage.body_color) 51 | 52 | garbage = garbage_block(6, 1, 4) 53 | assert.are.equal(4, garbage.body_color) 54 | end) 55 | 56 | it("色が正しくない場合エラー", function() 57 | assert.has.errors(function() garbage_block(3, 1, 0) end) 58 | assert.has.errors(function() garbage_block(3, 1, 1) end) 59 | assert.has.errors(function() garbage_block(3, 1, 5) end) 60 | assert.has.errors(function() garbage_block(3, 1, 6) end) 61 | assert.has.errors(function() garbage_block(3, 1, 7) end) 62 | assert.has.errors(function() garbage_block(3, 1, 8) end) 63 | assert.has.errors(function() garbage_block(3, 1, 9) end) 64 | assert.has.errors(function() garbage_block(3, 1, 10) end) 65 | assert.has.errors(function() garbage_block(3, 1, 11) end) 66 | assert.has.errors(function() garbage_block(3, 1, 12) end) 67 | assert.has.errors(function() garbage_block(3, 1, 13) end) 68 | assert.has.errors(function() garbage_block(3, 1, 14) end) 69 | assert.has.errors(function() garbage_block(3, 1, 15) end) 70 | end) 71 | 72 | it("色に応じて inner_border_color をセットする", function() 73 | assert.are.equal(14, garbage_block(6, 1, 2).inner_border_color) 74 | assert.are.equal(11, garbage_block(6, 1, 3).inner_border_color) 75 | assert.are.equal(9, garbage_block(6, 1, 4).inner_border_color) 76 | end) 77 | end) 78 | 79 | it("指定した幅と高さで board 上の位置を占有する", function() 80 | local board = board_class() 81 | 82 | -- _ _ _ _ _ _ 83 | -- _ ■ ■ ■ _ _ 84 | -- _ ■ ■ ■ _ _ 85 | -- _ ■ ■ ■ _ _ 86 | -- _ _ _ _ _ _ 87 | board:put(2, 2, garbage_block(3, 3)) 88 | 89 | -- 5 行目は空 90 | assert.is_true(board:is_empty(1, 5)) 91 | assert.is_true(board:is_empty(2, 5)) 92 | assert.is_true(board:is_empty(3, 5)) 93 | assert.is_true(board:is_empty(4, 5)) 94 | assert.is_true(board:is_empty(5, 5)) 95 | assert.is_true(board:is_empty(6, 5)) 96 | 97 | -- 4 行目は x = 1, 5, 6 のみ空 98 | assert.is_true(board:is_empty(1, 4)) 99 | assert.is_false(board:is_empty(2, 4)) 100 | assert.is_false(board:is_empty(3, 4)) 101 | assert.is_false(board:is_empty(4, 4)) 102 | assert.is_true(board:is_empty(5, 4)) 103 | assert.is_true(board:is_empty(6, 4)) 104 | 105 | -- 3 行目も x = 1, 5, 6 のみ空 106 | assert.is_true(board:is_empty(1, 3)) 107 | assert.is_false(board:is_empty(2, 3)) 108 | assert.is_false(board:is_empty(3, 3)) 109 | assert.is_false(board:is_empty(4, 3)) 110 | assert.is_true(board:is_empty(5, 3)) 111 | assert.is_true(board:is_empty(6, 3)) 112 | 113 | -- 2 行目も x = 1, 5, 6 のみ空 114 | assert.is_true(board:is_empty(1, 2)) 115 | assert.is_false(board:is_empty(2, 2)) 116 | assert.is_false(board:is_empty(3, 2)) 117 | assert.is_false(board:is_empty(4, 2)) 118 | assert.is_true(board:is_empty(5, 2)) 119 | assert.is_true(board:is_empty(6, 2)) 120 | 121 | -- 1 行目は空 122 | assert.is_true(board:is_empty(1, 1)) 123 | assert.is_true(board:is_empty(2, 1)) 124 | assert.is_true(board:is_empty(3, 1)) 125 | assert.is_true(board:is_empty(4, 1)) 126 | assert.is_true(board:is_empty(5, 1)) 127 | assert.is_true(board:is_empty(6, 1)) 128 | end) 129 | 130 | describe('render', function() 131 | it('おじゃまブロックの本体、影、内側の枠線を描画', function() 132 | local garbage = garbage_block(6, 1, 2) 133 | spy.on(garbage, "_render_box") 134 | 135 | garbage:render(50, 50) 136 | 137 | assert.spy(garbage._render_box).was.called(3) 138 | end) 139 | end) 140 | end) 141 | -------------------------------------------------------------------------------- /scripts/export_game_release.p8: -------------------------------------------------------------------------------- 1 | pico-8 cartridge // http://www.pico-8.com 2 | version 16 3 | __lua__ 4 | -- Run this commandline script with: 5 | -- $ pico8 -x export_cartridge.p8 6 | 7 | -- It will export .bin and .p8.png for the current game release 8 | -- Make sure to ./build_game release && ./install_cartridges.sh first 9 | -- Note that it will not warn if cartridge is not found. 10 | -- Paths are relative to PICO-8 carts directory. 11 | 12 | -- #version 13 | -- PICO-8 cannot read data/version.txt, so exceptionally set the version manually here 14 | local version = "0.7.5" 15 | local export_folder = "quantattack/v"..version.."_release" 16 | local game_basename = "quantattack_v"..version.."_release" 17 | local rel_png_folder = game_basename.."_png_cartridges" 18 | 19 | cd(export_folder) 20 | 21 | local entry_cartridge = "quantattack_title.p8" 22 | 23 | -- #cartridge (tagged to easily find what code to change when adding a new cartridge, 24 | -- and because this script cannot access external files like cartridges.txt) 25 | local additional_main_cartridges_list = { 26 | "quantattack_tutorial.p8", 27 | "quantattack_rush.p8", 28 | "quantattack_endless.p8", 29 | "quantattack_vs_qpu.p8", 30 | "quantattack_qpu_vs_qpu.p8", 31 | "quantattack_vs_human.p8", 32 | } 33 | 34 | -- all main cartridges, including entry cartridge 35 | local main_cartridges_list = {entry_cartridge} 36 | for additional_main_cartridge in all(additional_main_cartridges_list) do 37 | add(main_cartridges_list, additional_main_cartridge) 38 | end 39 | 40 | -- local data_cartridges_list = { 41 | -- -- remember that with the new install_data_cartridges_with_merging.sh, 42 | -- -- data_stage1_00.p8 in carts install folder now contains __gfx__ for start cinematic 43 | -- -- this allows us to stay just at the limit of 16 cartridges (including main cartridges) 44 | -- "data_stage1_00.p8", "data_stage1_10.p8", "data_stage1_20.p8", "data_stage1_30.p8", 45 | -- "data_stage1_01.p8", "data_stage1_11.p8", "data_stage1_21.p8", "data_stage1_31.p8", 46 | -- "data_stage1_intro.p8", "data_stage1_ingame.p8", 47 | -- "data_stage_sonic.p8" 48 | -- } 49 | 50 | -- PNG 51 | 52 | -- prepare folder for png cartridges 53 | printh("mkdir: " .. rel_png_folder) 54 | mkdir(rel_png_folder) 55 | 56 | -- -- data do not contain any code, so no need to adapt reload ".p8" -> ".p8.png" 57 | -- -- so just save them directly as png 58 | -- for cartridge_name in all(data_cartridges_list) do 59 | -- load(cartridge_name) 60 | -- save(rel_png_folder.."/"..cartridge_name..".png") 61 | -- end 62 | 63 | -- main cartridges need to be *adapted for PNG* for reload, so load those adapted versions 64 | -- to resave them as PNG 65 | -- (export_and_patch_cartridge_release.sh must have called pico-boots/scripts/adapt_for_png.py) 66 | -- the metadata label is used automatically for each 67 | cd("p8_for_png") 68 | 69 | for cartridge_name in all(main_cartridges_list) do 70 | load(cartridge_name) 71 | -- save as png (make sure to go one level up first since we are one level down) 72 | save("../"..rel_png_folder.."/"..cartridge_name..".png") 73 | end 74 | 75 | cd("..") 76 | 77 | printh("Resaved (adapted) cartridges as PNG in carts/"..export_folder.."/"..rel_png_folder) 78 | 79 | 80 | -- BIN & WEB 81 | 82 | -- load the original (not adapted for PNG) entry cartridge (titlemenu) 83 | -- this will serve as main entry point for the whole game 84 | load(entry_cartridge) 85 | 86 | -- concatenate cartridge names with space separator with a very simplified version 87 | -- of string.lua > joinstr_table that doesn't mind about adding an initial space 88 | local additional_cartridges_string = "" 89 | for cartridge_name in all(additional_main_cartridges_list) do 90 | additional_cartridges_string = additional_cartridges_string.." "..cartridge_name 91 | end 92 | for cartridge_name in all(data_cartridges_list) do 93 | additional_cartridges_string = additional_cartridges_string.." "..cartridge_name 94 | end 95 | 96 | 97 | -- BIN 98 | 99 | -- exports are done via EXPORT, and can use a custom icon 100 | -- instead of the .p8.png label 101 | -- icon is stored in builtin_data_titlemenu.p8, 102 | -- as a 16x16 square => -s 2 tiles wide 103 | -- with top-left cell at sprite 46 (run1) => -i 46 104 | -- on pink (color 14) background => -c 14 105 | -- and most importantly we pass additional logic and data files as additional cartridges 106 | export(game_basename..".bin "..additional_cartridges_string.." -i 46 -s 2 -c 14") 107 | printh("Exported binaries in carts/"..export_folder.."/"..game_basename..".bin") 108 | 109 | 110 | -- WEB 111 | 112 | mkdir(game_basename.."_web") 113 | -- Do not cd into game_basename.."_web" because we want the additional cartridges to be accessible 114 | -- in current path. Instead, export directly into the _web folder 115 | -- Use custom template. It is located in plates/quantattack_template.html and copied into PICO-8 config dir plates 116 | -- in export_and_patch_cartridge_release.sh 117 | export(game_basename.."_web/"..game_basename..".html "..additional_cartridges_string.." -i 46 -s 2 -c 14 -p crtplate") 118 | printh("Exported HTML in carts/"..export_folder.."/"..game_basename..".html") 119 | 120 | cd("..") 121 | -------------------------------------------------------------------------------- /src/main_tutorial.lua: -------------------------------------------------------------------------------- 1 | require("lib/helpers") 2 | require("tutorial/dtb") 3 | require("lib/effects") 4 | require("lib/board") 5 | require("lib/player") 6 | 7 | local game_class = require("tutorial/game") 8 | 9 | local cursor = cursor_class() 10 | local board, player = board_class(cursor), player_class() 11 | local tutorial_game = game_class() 12 | 13 | local ion_class = require("tutorial/ion") 14 | local ion = ion_class() 15 | 16 | local _wait_sec, _wait_callback 17 | local show_legends = false 18 | 19 | function wait(wait_sec, callback) 20 | _main_state = ":wait" 21 | _time_wait_start = t() 22 | _wait_sec = wait_sec 23 | _wait_callback = callback 24 | end 25 | 26 | function tutorial_game.reduce_callback(_score, _player) 27 | -- 消えてないブロックが残っていれば、コールバック本体を呼ばない 28 | for _y = 1, board.rows do 29 | for _x = 1, board.cols do 30 | local block_xy = board.blocks[_y][_x] 31 | if block_xy.state == "idle" and not (block_xy.type == "i" or block_xy.type == "#") then 32 | return 33 | end 34 | end 35 | end 36 | 37 | wait(1, function() 38 | dtb_disp(({ "awesome!", "great!", "nice!" })[flr(rnd(3)) + 1]) 39 | ion:shake() 40 | show_legends = false 41 | tutorial_game.move_cursor = false 42 | _main_state = _next_state_after_clear 43 | end) 44 | end 45 | 46 | _main_state = nil 47 | dtb_init() 48 | 49 | local board_data_h = { 50 | { "#", "#", "h", "i", "#", "#" }, 51 | { "#", "#", "#", "h", "#", "#" }, 52 | { "#", "#", "#", "#", "#", "#" }, 53 | { "#", "#", "#", "#", "#", "#" }, 54 | { "#", "#", "#", "#", "#", "#" }, 55 | { "#", "#", "#", "#", "#", "#" }, 56 | { "#", "#", "#", "#", "#", "#" }, 57 | { "#", "#", "#", "#", "#", "#" } 58 | } 59 | 60 | local board_data_xy = { 61 | { "i", "y", "i", "i", "i", "y" }, 62 | { "#", "#", "x", "i", "#", "#" }, 63 | { "#", "#", "#", "x", "#", "#" }, 64 | { "#", "#", "#", "#", "#", "#" }, 65 | { "#", "#", "#", "#", "#", "#" }, 66 | { "#", "#", "#", "#", "#", "#" }, 67 | { "#", "#", "#", "#", "#", "#" }, 68 | { "#", "#", "#", "#", "#", "#" } 69 | } 70 | 71 | local board_data_s = { 72 | { "s", "s", "i", "i", "i", "s" }, 73 | { "#", "#", "#", "i", "#", "#" }, 74 | { "#", "#", "#", "s", "#", "#" }, 75 | { "#", "#", "#", "#", "#", "#" }, 76 | { "#", "#", "#", "#", "#", "#" }, 77 | { "#", "#", "#", "#", "#", "#" }, 78 | { "#", "#", "#", "#", "#", "#" }, 79 | { "#", "#", "#", "#", "#", "#" } 80 | } 81 | 82 | local board_data_cnot = { 83 | { "cnot_x,5", "i", "i", "i", "control,1", "i" }, 84 | { "i", "control,3", "cnot_x,2", "#", "#", "#" }, 85 | { "#", "#", "#", "#", "#", "#" }, 86 | { "#", "#", "#", "#", "#", "#" }, 87 | { "#", "#", "#", "#", "#", "#" }, 88 | { "#", "#", "#", "#", "#", "#" }, 89 | { "#", "#", "#", "#", "#", "#" }, 90 | { "#", "#", "#", "#", "#", "#" } 91 | } 92 | 93 | function _init() 94 | _main_state = ":ion_appear" 95 | 96 | player:init() 97 | board:init() 98 | 99 | cursor:init() 100 | 101 | tutorial_game.player = player 102 | tutorial_game.cursor = cursor 103 | tutorial_game.board = board 104 | tutorial_game:_init() 105 | 106 | ion:appear(function() 107 | ion:shake(function() 108 | dtb_disp("hi! my name is ion.", function() 109 | dtb_disp("let me introduce the rules of this game.", function() 110 | tutorial_game:raise_stack(board_data_h, function() 111 | _main_state = ":how_to_play" 112 | _next_state_after_clear = ":try_xy" 113 | end) 114 | end) 115 | end) 116 | end) 117 | end) 118 | end 119 | 120 | function _update60() 121 | if _main_state == ":how_to_play" then 122 | dtb_disp("your mission is to clear these blocks so they do not reach the top.") 123 | dtb_disp("first, let's clear the two red h blocks.") 124 | dtb_disp("line up these two h blocks vertically by swapping the blocks.", function() 125 | _main_state = ":try_h_h" 126 | show_legends = true 127 | tutorial_game.move_cursor = true 128 | end) 129 | _main_state = "idle" 130 | elseif _main_state == ":wait" then 131 | if _wait_sec < t() - _time_wait_start then 132 | _wait_callback() 133 | end 134 | elseif _main_state == ":try_xy" then 135 | dtb_disp("how about this then?", function() 136 | show_legends = true 137 | tutorial_game.move_cursor = true 138 | tutorial_game:raise_stack(board_data_xy) 139 | _next_state_after_clear = ":try_s" 140 | end) 141 | _main_state = "idle" 142 | elseif _main_state == ":try_s" then 143 | dtb_disp("some blocks change into other blocks!", function() 144 | show_legends = true 145 | tutorial_game.move_cursor = true 146 | tutorial_game:raise_stack(board_data_s) 147 | _next_state_after_clear = ":try_cnot" 148 | end) 149 | _main_state = "idle" 150 | elseif _main_state == ":try_cnot" then 151 | dtb_disp("some of the blocks are a bit odd...", function() 152 | tutorial_game:raise_stack(board_data_cnot) 153 | _next_state_after_clear = ":fin" 154 | end) 155 | dtb_disp("this is two connected blocks, and they're not easy to clear.") 156 | dtb_disp("can you clear them?", function() 157 | show_legends = true 158 | tutorial_game.move_cursor = true 159 | end) 160 | _main_state = "idle" 161 | elseif _main_state == ":fin" then 162 | dtb_disp("wow. you are catching on fast!") 163 | dtb_disp("i have given you all the basic rules.") 164 | dtb_disp("there are many secret rules hidden in the game, so discover them.") 165 | dtb_disp("well, see you around!", function() 166 | jump("quantattack_title") 167 | end) 168 | _main_state = "idle" 169 | end 170 | 171 | tutorial_game:update() 172 | dtb_update() 173 | ion:update() 174 | end 175 | 176 | function _draw() 177 | cls() 178 | 179 | ripple:render_all() 180 | tutorial_game:render() 181 | dtb_draw() 182 | ion:draw() 183 | 184 | if show_legends then 185 | spr(99, 70, 119) 186 | print_outlined("swap blocks", 81, 120, 7, 0) 187 | end 188 | end 189 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'bundler/setup' 4 | $LOAD_PATH.unshift File.expand_path('lib', __dir__) 5 | 6 | require 'rake/clean' 7 | 8 | def version 9 | File.read('data/version.txt').chomp 10 | end 11 | 12 | def carts 13 | %w[title tutorial endless rush vs_qpu qpu_vs_qpu vs_human] 14 | end 15 | 16 | def p8_path(name, build = :debug) 17 | "build/v#{version}_#{build}/quantattack_#{name}.p8" 18 | end 19 | 20 | def cart_main_source(name) 21 | "src/main_#{name}.lua" 22 | end 23 | 24 | def cart_specific_sources(name) 25 | FileList["src/#{name}/*.lua"] 26 | end 27 | 28 | def common_lib_sources 29 | FileList['src/lib/**/*.lua'] 30 | end 31 | 32 | def cart_sources(name) 33 | FileList[cart_main_source(name)] + cart_specific_sources(name) + common_lib_sources 34 | end 35 | 36 | def cart_data(name) 37 | "data/builtin_data_#{name}.p8" 38 | end 39 | 40 | def cart_sources_and_data(name) 41 | cart_sources(name) + FileList[cart_data(name)] 42 | end 43 | 44 | def build_cartridge_command(name, build = :debug, *symbols) 45 | symbols_option = if build == :debug 46 | ([:tostring, :dump, name] + symbols).uniq.join(',') 47 | else 48 | ([name] + symbols).join(',') 49 | end 50 | "./pico-boots/scripts/build_cartridge.sh \"#{File.join(__dir__, './src')}\" main_#{name}.lua " \ 51 | "-d \"#{File.join(__dir__, "data/builtin_data_#{name}.p8")}\" " \ 52 | "-M \"#{File.join(__dir__, 'data/metadata.p8')}\" " \ 53 | "-a yasuhito -t \"quantattack v#{version} (#{name})\" " \ 54 | "-p \"#{File.join(__dir__, "build/v#{version}_#{build}")}\" " \ 55 | "-o quantattack_#{name} " \ 56 | "-s \"#{symbols_option}\" " \ 57 | '--minify-level 1' 58 | end 59 | 60 | def build_single_cartridge_sh 61 | './scripts/build_single_cartridge.sh' 62 | end 63 | 64 | def install_single_cartridge_sh 65 | './scripts/install_single_cartridge.sh' 66 | end 67 | 68 | def pmd_sh 69 | '~/Documents/GitHub/pmd-bin-6.52.0/bin/run.sh' 70 | end 71 | 72 | desc '現在のバージョンを表示' 73 | task :version do 74 | puts version 75 | end 76 | 77 | task default: 'build:debug' 78 | 79 | desc 'リリース' 80 | task release: 'build:release' do 81 | sh './scripts/export_and_patch_cartridge_release.sh' 82 | end 83 | 84 | desc 'data カートを起動' 85 | task :data do 86 | sh '/Applications/PICO-8.app/Contents/MacOS/pico8 -run data.p8 -screenshot_scale 4 -gif_scale 4' 87 | end 88 | 89 | task :test do 90 | sh './scripts/test.sh' 91 | end 92 | 93 | task 'test:solo' do 94 | sh './scripts/test.sh -m solo' 95 | end 96 | 97 | desc 'デバッグビルド' 98 | task 'build:debug' => carts 99 | 100 | desc 'リリースビルド' 101 | task 'build:release' => carts.map { |each| "#{each}:release" } 102 | 103 | carts.each do |each| 104 | file cart_data(each) => 'data.p8' do 105 | cp 'data.p8', cart_data(each) 106 | end 107 | end 108 | 109 | task title: p8_path('title') 110 | file p8_path('title') => cart_sources_and_data('title') do 111 | sh build_cartridge_command(:title) 112 | sh "#{install_single_cartridge_sh} title debug" 113 | end 114 | 115 | task tutorial: p8_path('tutorial') 116 | file p8_path('tutorial') => cart_sources_and_data('tutorial') do 117 | sh build_cartridge_command(:tutorial) 118 | sh "#{install_single_cartridge_sh} tutorial debug" 119 | end 120 | 121 | task endless: p8_path('endless') 122 | file p8_path('endless') => cart_sources_and_data('endless') do 123 | sh build_cartridge_command(:endless, :debug, :sash) 124 | sh "#{install_single_cartridge_sh} endless debug" 125 | end 126 | 127 | task rush: p8_path('rush') 128 | file p8_path('rush') => cart_sources_and_data('rush') do 129 | sh build_cartridge_command(:rush, :debug, :sash) 130 | sh "#{install_single_cartridge_sh} rush debug" 131 | end 132 | 133 | task vs_qpu: p8_path('vs_qpu') 134 | file p8_path('vs_qpu') => cart_sources_and_data('vs_qpu') do 135 | sh build_cartridge_command(:vs_qpu) 136 | sh "#{install_single_cartridge_sh} vs_qpu debug" 137 | end 138 | 139 | task qpu_vs_qpu: p8_path('qpu_vs_qpu') 140 | file p8_path('qpu_vs_qpu') => cart_sources_and_data('qpu_vs_qpu') do 141 | sh build_cartridge_command(:qpu_vs_qpu) 142 | sh "#{install_single_cartridge_sh} qpu_vs_qpu debug" 143 | end 144 | 145 | task vs_human: p8_path('vs_human') 146 | file p8_path('vs_human') => cart_sources_and_data('vs_human') do 147 | sh build_cartridge_command(:vs_human) 148 | sh "#{install_single_cartridge_sh} vs_human debug" 149 | end 150 | 151 | task 'title:release' => p8_path('title', :release) 152 | file p8_path('title', :release) => cart_sources_and_data('title') do 153 | sh build_cartridge_command(:title, :release) 154 | sh "#{install_single_cartridge_sh} title release" 155 | end 156 | 157 | task 'tutorial:release' => p8_path('tutorial', :release) 158 | file p8_path('tutorial', :release) => cart_sources_and_data('tutorial') do 159 | sh build_cartridge_command(:tutorial, :release) 160 | sh "#{install_single_cartridge_sh} tutorial release" 161 | end 162 | 163 | task 'endless:release' => p8_path('endless', :release) 164 | file p8_path('endless', :release) => cart_sources_and_data('endless') do 165 | sh build_cartridge_command(:endless, :release, :sash) 166 | sh "#{install_single_cartridge_sh} endless release" 167 | end 168 | 169 | task 'rush:release' => p8_path('rush', :release) 170 | file p8_path('rush', :release) => cart_sources_and_data('rush') do 171 | sh build_cartridge_command(:rush, :release, :sash) 172 | sh "#{install_single_cartridge_sh} rush release" 173 | end 174 | 175 | task 'vs_qpu:release' => p8_path('vs_qpu', :release) 176 | file p8_path('vs_qpu', :release) => cart_sources_and_data('vs_qpu') do 177 | sh build_cartridge_command(:vs_qpu, :release) 178 | sh "#{install_single_cartridge_sh} vs_qpu release" 179 | end 180 | 181 | task 'qpu_vs_qpu:release' => p8_path('qpu_vs_qpu', :release) 182 | file p8_path('qpu_vs_qpu', :release) => cart_sources_and_data('qpu_vs_qpu') do 183 | sh build_cartridge_command(:qpu_vs_qpu, :release) 184 | sh "#{install_single_cartridge_sh} qpu_vs_qpu release" 185 | end 186 | 187 | task 'vs_human:release' => p8_path('vs_human', :release) 188 | file p8_path('vs_human', :release) => cart_sources_and_data('vs_human') do 189 | sh build_cartridge_command(:vs_human, :release) 190 | sh "#{install_single_cartridge_sh} vs_human release" 191 | end 192 | 193 | carts.each do |each| 194 | CLOBBER.include p8_path(each) 195 | CLOBBER.include p8_path(each, :release) 196 | end 197 | 198 | desc 'title カートを起動' 199 | task run: 'build:debug' do 200 | sh "/Applications/PICO-8.app/Contents/MacOS/pico8 -run #{p8_path 'title'} -screenshot_scale 4 -gif_scale 4" 201 | end 202 | 203 | task count: 'build:debug' do 204 | shrinko8 = 'python ~/Documents/GitHub/shrinko8/shrinko8.py' 205 | 206 | carts.each do |each| 207 | output = `#{shrinko8} #{p8_path each} --parsable-count --count` 208 | if /count:None:tokens:(\d+)/=~ output 209 | tokens = Regexp.last_match(1).to_i 210 | 211 | puts "#{each}: #{tokens} (#{(tokens / 8192.0 * 100).round(1)} %)" 212 | else 213 | warn 'Invalid output' 214 | end 215 | end 216 | end 217 | 218 | task :dupe do 219 | sh "#{pmd_sh} cpd --dir src/ --language lua --minimum-tokens 10" 220 | end 221 | -------------------------------------------------------------------------------- /test/board/swap_utest.lua: -------------------------------------------------------------------------------- 1 | require("engine/test/bustedhelper") 2 | require("test/test_helper") 3 | require("lib/effects") 4 | require("lib/board") 5 | 6 | describe('board', function() 7 | local board 8 | 9 | before_each(function() 10 | board = board_class() 11 | end) 12 | 13 | describe('swap', function() 14 | describe('単一ブロック同士の入れ換え', function() 15 | before_each(function() 16 | board:put(1, 1, block_class("h")) 17 | board:put(2, 1, block_class("x")) 18 | end) 19 | 20 | it('swap は true を返す', function() 21 | assert.is_true(board:swap(1, 1)) 22 | end) 23 | 24 | it('ブロックの状態を swapping にする', function() 25 | board:swap(1, 1) 26 | 27 | assert.is_true(board:block_at(1, 1).state == "swap") 28 | end) 29 | 30 | -- TODO: フレーム数のテストは別のテストに分離 31 | it("3 フレームで swapping 状態が完了する", function() 32 | board:swap(1, 1) 33 | 34 | -- フレーム 1: swap 開始 35 | board:update() 36 | assert.is_true(board:block_at(1, 1).state == "swap") 37 | assert.is_true(board:block_at(2, 1).state == "swap") 38 | 39 | -- フレーム 2 40 | board:update() 41 | assert.is_true(board:block_at(1, 1).state == "swap") 42 | assert.is_true(board:block_at(2, 1).state == "swap") 43 | 44 | -- フレーム 3: swap 終了 45 | board:update() 46 | assert.is_true(board:block_at(1, 1).state == "swap") 47 | assert.is_true(board:block_at(2, 1).state == "swap") 48 | 49 | -- フレーム 4: idle 状態に遷移 50 | board:update() 51 | assert.is_true(board:block_at(1, 1).state == "idle") 52 | assert.is_true(board:block_at(2, 1).state == "idle") 53 | end) 54 | 55 | it("ブロックを入れ換える", function() 56 | board:swap(1, 1) 57 | board:update() -- swapping (3 フレーム) 58 | board:update() 59 | board:update() 60 | board:update() -- idle 状態に遷移 61 | 62 | assert.are.equal("x", board:block_at(1, 1).type) 63 | assert.are.equal("h", board:block_at(2, 1).type) 64 | end) 65 | end) 66 | 67 | describe('単一ブロックと I ブロックの入れ換え', function() 68 | before_each(function() 69 | board:put(1, 1, block_class("h")) 70 | end) 71 | 72 | it("ブロックを入れ換える", function() 73 | board:swap(1, 1) 74 | board:update() -- swapping (3 フレーム) 75 | board:update() 76 | board:update() 77 | board:update() -- idle 状態に遷移 78 | 79 | assert.are.equal("i", board:block_at(1, 1).type) 80 | assert.are.equal("h", board:block_at(2, 1).type) 81 | end) 82 | end) 83 | 84 | describe('I ブロック同士の入れ換え', function() 85 | it("swap は true を返す", function() 86 | assert.is_true(board:swap(1, 1)) 87 | end) 88 | end) 89 | 90 | describe("プレースホルダブロック (#) の入れ換え", function() 91 | before_each(function() 92 | board:put(2, 1, block_class("#")) 93 | end) 94 | 95 | it("swap は false を返す", function() 96 | assert.is_false(board:swap(1, 1)) -- 右に # がある場合 97 | assert.is_false(board:swap(2, 1)) -- 左に # がある場合 98 | end) 99 | end) 100 | 101 | describe("おじゃまゲートの入れ換え", function() 102 | before_each(function() 103 | board:put(2, 1, garbage_block(3)) 104 | end) 105 | 106 | it("swap は false を返す", function() 107 | assert.is_false(board:swap(1, 1)) -- 右におじゃまゲートがある場合 108 | assert.is_false(board:swap(4, 1)) -- 左におじゃまゲートがある場合 109 | end) 110 | end) 111 | 112 | describe("hover 状態のブロックとの入れ替え", function() 113 | local left_block, right_block 114 | 115 | before_each(function() 116 | left_block = block_class("h") 117 | right_block = block_class("x") 118 | 119 | board:put(1, 1, left_block) 120 | board:put(2, 1, right_block) 121 | end) 122 | 123 | it("左のブロックが hover 状態の場合、swap は false を返す", function() 124 | left_block:hover() 125 | 126 | assert.is_false(board:swap(1, 1)) 127 | end) 128 | 129 | it("右のブロックが hover 状態の場合、swap は false を返す", function() 130 | right_block:hover() 131 | 132 | assert.is_false(board:swap(1, 1)) 133 | end) 134 | end) 135 | 136 | describe("swapping 状態のブロックとの入れ替え", function() 137 | before_each(function() 138 | board:put(2, 1, block_class("h")) 139 | end) 140 | 141 | it("左のブロックが swapping 状態の場合、swap は false を返す", function() 142 | board:swap(1, 1) 143 | 144 | assert.is_false(board:swap(2, 1)) 145 | end) 146 | 147 | it("右のブロックが swapping 状態の場合、swap は false を返す", function() 148 | board:swap(2, 1) 149 | 150 | assert.is_false(board:swap(1, 1)) 151 | end) 152 | end) 153 | 154 | describe("fall 状態のブロックとの入れ替え", function() 155 | local h_gate 156 | 157 | before_each(function() 158 | h_gate = block_class("h") 159 | board:put(2, 16, h_gate) 160 | h_gate:fall() 161 | end) 162 | 163 | it("左のブロックが fall 状態の場合、swap は true を返す", function() 164 | assert.is_true(board:swap(2, 16)) 165 | end) 166 | 167 | it("右のブロックが fall 状態の場合、swap は true を返す", function() 168 | assert.is_true(board:swap(1, 16)) 169 | end) 170 | end) 171 | 172 | describe("match 状態のブロックとの入れ替え", function() 173 | local h_gate 174 | 175 | before_each(function() 176 | h_gate = block_class("h") 177 | board:put(2, 1, h_gate) 178 | h_gate.state = "match" 179 | end) 180 | 181 | it("左のブロックが match 状態の場合、swap は false を返す", function() 182 | assert.is_false(board:swap(2, 1)) 183 | end) 184 | 185 | it("右のブロックが match 状態の場合、swap は false を返す", function() 186 | assert.is_false(board:swap(1, 1)) 187 | end) 188 | end) 189 | 190 | describe("freeze 状態のブロックとの入れ替え", function() 191 | local h_gate 192 | 193 | before_each(function() 194 | h_gate = block_class("h") 195 | board:put(2, 16, h_gate) 196 | h_gate.state = "freeze" 197 | end) 198 | 199 | it("左のブロックが freeze 状態の場合、swap は false を返す", function() 200 | assert.is_false(board:swap(2, 16)) 201 | end) 202 | 203 | it("右のブロックが freeze 状態の場合、swap は false を返す", function() 204 | assert.is_false(board:swap(1, 16)) 205 | end) 206 | end) 207 | 208 | describe("CNOT の一部と単一ブロックの入れ替え", function() 209 | before_each(function() 210 | board:put(1, 1, block_class("h")) 211 | board:put(2, 1, control_block(3)) 212 | board:put(3, 1, cnot_x_block(2)) 213 | board:put(4, 1, block_class("h")) 214 | end) 215 | 216 | it("左が CNOT の一部、右が単一ブロックの場合、swap は false を返す", function() 217 | assert.is_false(board:swap(3, 1)) 218 | end) 219 | 220 | it("左が単一ブロック、右が CNOT の一部の場合、swap は false を返す", function() 221 | assert.is_false(board:swap(1, 1)) 222 | end) 223 | end) 224 | 225 | describe("SWAP の一部と単一ブロックの入れ替え", function() 226 | before_each(function() 227 | board:put(1, 1, block_class("h")) 228 | board:put(2, 1, swap_block(3)) 229 | board:put(3, 1, swap_block(2)) 230 | board:put(4, 1, block_class("h")) 231 | end) 232 | 233 | it("左が SWAP の一部、右が単一ブロックの場合、swap は false を返す", function() 234 | assert.is_false(board:swap(3, 1)) 235 | end) 236 | 237 | it("左が単一ブロック、右が SWAP の一部の場合、swap は false を返す", function() 238 | assert.is_false(board:swap(1, 1)) 239 | end) 240 | end) 241 | end) 242 | end) 243 | -------------------------------------------------------------------------------- /src/lib/effects.lua: -------------------------------------------------------------------------------- 1 | ---@diagnostic disable: lowercase-global 2 | 3 | --- すべてのエフェクトのベースクラス 4 | effect_set = new_class() 5 | 6 | function effect_set:_init() 7 | self.all = {} 8 | end 9 | 10 | function effect_set:_add(f) 11 | local _ENV = setmetatable({}, { __index = _ENV }) 12 | f(_ENV) 13 | add(self.all, _ENV) 14 | end 15 | 16 | function effect_set:update_all() 17 | foreach(self.all, function(each) 18 | self._update(each, self.all) 19 | end) 20 | end 21 | 22 | function effect_set:render_all() 23 | foreach(self.all, function(each) 24 | self._render(each) 25 | end) 26 | end 27 | 28 | 29 | --- 各種エフェクト 30 | 31 | --- 32 | -- bubbles: 同時消しまたは連鎖の数を表示 33 | -- 34 | 35 | bubbles = derived_class(effect_set)() 36 | 37 | --- バブルを作る 38 | function bubbles:create(bubble_type, count, coord) 39 | self:_add(function(_ENV) 40 | _type, _count, _x, _y, _tick = bubble_type, count, coord[1], coord[2] - 8, 0 41 | end) 42 | end 43 | 44 | function bubbles._update(_ENV, all) 45 | if _tick > 40 then 46 | del(all, _ENV) 47 | end 48 | if _tick < 30 then 49 | _y = _y - 0.2 50 | end 51 | 52 | _tick = _tick + 1 53 | end 54 | 55 | function bubbles._render(_ENV) 56 | if _type == "combo" then 57 | draw_rounded_box(_x - 1, _y + 1, _x + 7, _y + 9, 5, 5) 58 | draw_rounded_box(_x - 1, _y, _x + 7, _y + 8, 7, 8) 59 | 60 | cursor(_x + 2, _y + 2) 61 | else 62 | local rbox_dx = _count < 10 and 0 or -2 63 | 64 | draw_rounded_box(_x + rbox_dx - 2, _y + 1, _x - rbox_dx + 8, _y + 9, 5, 5) 65 | draw_rounded_box(_x + rbox_dx - 2, _y, _x - rbox_dx + 8, _y + 8, 7, 3) 66 | 67 | spr(96, _x + rbox_dx, _y - 1) -- the "x" part in "x5" 68 | 69 | cursor(_x + rbox_dx + 4, _y + 2) 70 | end 71 | 72 | print(_count, 10) 73 | end 74 | 75 | 76 | --- 77 | -- ions: 攻撃または相殺の時に表示するイオン球エフェクト 78 | 79 | ions = derived_class(effect_set)() 80 | 81 | --- イオン球エフェクトを作る 82 | -- 83 | -- 例: (64, 64) から (10, 10) へ青色 (デフォルト) のイオン球を飛ばす 84 | -- ions:create({ 64, 64 }, { 10, 10 }, ions_callback) 85 | -- 86 | function ions:create(from, target, callback, ion_color) 87 | self:_add(function(_ENV) 88 | _from_x, _from_y, _target, _callback, _color, _tick = 89 | from[1], from[2], target, callback, ion_color or 12, 0 90 | sfx(20) 91 | end) 92 | end 93 | 94 | --- イオン球の位置等を更新 95 | function ions._update(_ENV, all) 96 | local _quadratic_bezier = function(from, mid, to) 97 | local t = _tick / 60 98 | return (1 - t) * (1 - t) * from + 2 * (1 - t) * t * mid + t * t * to 99 | end 100 | 101 | if _tick == 60 then 102 | _callback(_target) 103 | del(all, _ENV) 104 | end 105 | 106 | _x, _y, _tick = 107 | _quadratic_bezier(_from_x, _from_x > 64 and _from_x + 60 or _from_x - 60, _target[1]), 108 | _quadratic_bezier(_from_y, _from_y + 40, _target[2]), 109 | _tick + 1 110 | 111 | -- しっぽを追加 112 | if ceil_rnd(10) > 7 then 113 | particles:create( 114 | { _x, _y }, 115 | "3,2," .. _color .. "," .. _color .. ",0,0,0,0,20" 116 | ) 117 | end 118 | end 119 | 120 | --- イオン球を描画 121 | function ions._render(_ENV) 122 | fillp(23130.5) 123 | circfill(_x, _y, 8 + 2 * sin(t()), _color) 124 | fillp() 125 | circfill(_x, _y, 6 + 2 * sin(1.5 * t()), _color) 126 | circfill(_x, _y, 5 + sin(2.5 * t()), 7) 127 | end 128 | 129 | 130 | --- 131 | -- パーティクルを表示 132 | 133 | particles = derived_class(effect_set)() 134 | 135 | --- パーティクルの集合を作る 136 | function particles:create(coord, data) 137 | foreach(split(data, "|"), function(each) 138 | radius, end_radius, particle_color, particle_color_fade, dx, dy, ddx, ddy, max_tick = unpack_split(each) 139 | 140 | self:_add(function(_ENV) 141 | _x, _y, _dx, _dy, _radius, _end_radius, _color, _color_fade, _tick, _max_tick, _ddx, _ddy = 142 | coord[1], coord[2], dx == "" and rnd(1) or dx, dy == "" and rnd(1) or dy, radius, end_radius, particle_color, 143 | particle_color_fade, 144 | 0, max_tick + rnd(10), ddx, ddy 145 | 146 | if dx == "" then 147 | -- move to the left 148 | if ceil_rnd(2) == 1 then 149 | _dx, _ddx = _dx * -1, _ddx * -1 150 | end 151 | 152 | -- move upwards 153 | if ceil_rnd(2) == 1 then 154 | _dy, _ddy = _dy * -1, _ddy * -1 155 | end 156 | end 157 | end) 158 | end) 159 | end 160 | 161 | --- パーティクルの位置等を更新 162 | function particles._update(_ENV, all) 163 | if _tick > _max_tick then 164 | del(all, _ENV) 165 | end 166 | if _tick > _max_tick * .5 then 167 | _color, _radius = _color_fade, _end_radius 168 | end 169 | 170 | _x, _y, _dx, _dy, _tick = _x + _dx, _y + _dy, _dx + _ddx, _dy + _ddy, _tick + 1 171 | end 172 | 173 | --- パーティクルを描画 174 | function particles._render(_ENV) 175 | circfill(_x, _y, _radius, _color) 176 | end 177 | 178 | 179 | --#ifn title 180 | --- 背景の波紋を描画するクラス 181 | ripple = derived_class(effect_set)() 182 | 183 | function ripple:create() 184 | self:_add(function(_ENV) 185 | t1, t2, tick = 0, 0, 0 186 | end) 187 | end 188 | 189 | --- 波紋の状態を更新 190 | function ripple._update(_ENV) 191 | tick, t1, t2 = 192 | tick + 1, 193 | t1 - 1 / ((ripple.slow or ripple.freeze) and 3000 or 1500), 194 | t2 - 1 / ((ripple.slow or ripple.freeze) and 300 or 150) 195 | end 196 | 197 | --- 波紋を描画 198 | function ripple._render(_ENV) 199 | for i = -5, 5 do 200 | for j = -5, 5 do 201 | local ang, d = atan2(i, j), sqrt(i * i + j * j) 202 | local r = 2 + 2 * sin(d / 4 + t2) 203 | circfill( 204 | 64 + 12 * d * cos(ang + t1), 205 | 64 + 12 * d * sin(ang + t1) - 3 * r, 206 | r, 207 | ((ripple.slow or ripple.freeze) and r > 3 and tick % 2 == 0) and (ripple.slow and 13 or 12) or 1 208 | ) 209 | end 210 | end 211 | end 212 | 213 | ripple:create() 214 | --#endif 215 | 216 | --#if sash 217 | -- sash:create("text,text_color,background_color", slideout_callback) 新しい sash を作る (シングルトン) 218 | -- - text: 表示するテキスト 219 | -- - text_color: テキストの色 220 | -- - background_color: sash の背景色 221 | -- - slideout_callback: sash が右端から消えた時に呼ぶコールバック 222 | 223 | sash = derived_class(effect_set)() 224 | 225 | function sash:create(properties, _slideout_callback) 226 | self.all = {} 227 | self:_add(function (_ENV) 228 | text, text_color, background_color = unpack_split(properties) 229 | background_height, dh, ddh, slideout_callback, text_x, text_dx, text_ddx, text_center_x, state = 230 | 0, 0.1, 0.2, _slideout_callback, #text * -4, 5, -0.14, 64 - #text * 2, ":slidein" 231 | end) 232 | end 233 | 234 | function sash._update(_ENV) 235 | if state == ":slidein" then 236 | background_height, dh = background_height + dh, dh + ddh 237 | if background_height > 10 then 238 | background_height = 10 239 | end 240 | 241 | if text_x < text_center_x then 242 | text_x, text_dx = text_x + text_dx, text_dx + text_ddx 243 | end 244 | 245 | if text_x > text_center_x then 246 | text_x, time_stop, state = text_center_x, t(), ":stop" 247 | end 248 | end 249 | 250 | if state == ":stop" then 251 | if t() - time_stop > 1 then 252 | dh, ddh, text_dx, text_ddx, state = 253 | -0.1, -0.2, 3, 0.8, ":slideout" 254 | end 255 | end 256 | 257 | if state == ":slideout" then 258 | background_height, dh, text_x, text_dx = 259 | background_height + dh, dh + ddh, text_x + text_dx, text_dx + text_ddx 260 | 261 | if text_x > 127 then 262 | if slideout_callback then 263 | slideout_callback() 264 | end 265 | 266 | state = ":finished" 267 | end 268 | end 269 | end 270 | 271 | function sash._render(_ENV) 272 | if background_height > 0 then 273 | rectfill(0, 64 - background_height / 2, 127, 64 + background_height / 2, background_color) 274 | print(text, text_x, 64 - 2, text_color) 275 | end 276 | end 277 | --#endif 278 | -------------------------------------------------------------------------------- /scripts/export_and_patch_cartridge_release.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Export and patch cartridge releases, then update existing archives with patched executables 4 | # Also apply small tweaks to make release work completely: 5 | # - rename HTML file to index.html to make it playable directly in browser (esp. on itch.io) 6 | # - add '.png' to every occurrence of '.p8' in copy of game source before exporting to PNG 7 | # (to allow reload() to work with png cartridges) 8 | # Make sure to first build full game in release 9 | 10 | # Configuration: paths 11 | picoboots_scripts_path="$(dirname "$0")/../pico-boots/scripts" 12 | picoboots_plates_path="$(dirname "$0")/../pico-boots/plates" 13 | game_scripts_path="$(dirname "$0")" 14 | data_path="$(dirname "$0")/../data" 15 | # Linux only 16 | carts_dirpath="$HOME/.lexaloffle/pico-8/carts" 17 | config_plates_dirpath="$HOME/.lexaloffle/pico-8/plates" 18 | 19 | # Configuration: cartridge 20 | pico8_version=`cat "$data_path/pico8_version.txt"` 21 | version=`cat "$data_path/version.txt"` 22 | cartridge_stem="quantattack" 23 | export_folder="$carts_dirpath/${cartridge_stem}/v${version}_release" 24 | cartridge_basename="${cartridge_stem}_v${version}_release" 25 | 26 | # Verify that the export folder is present. This does not guarantee we built and installed all cartridges 27 | # to carts correctly, but if not present don't even try to export. 28 | if [[ ! -d "$export_folder" ]]; then 29 | echo "No directory found at $export_folder. Make sure to build and install the cartridges in release first. STOP." 30 | exit 1 31 | fi 32 | 33 | # Configuration: sub-directories 34 | rel_p8_folder="${cartridge_basename}_cartridges" 35 | rel_png_folder="${cartridge_basename}_png_cartridges" 36 | rel_bin_folder="${cartridge_basename}.bin" 37 | rel_web_folder="${cartridge_basename}_web" 38 | 39 | p8_folder="${export_folder}/${rel_p8_folder}" 40 | png_folder="${export_folder}/${rel_png_folder}" 41 | bin_folder="${export_folder}/${rel_bin_folder}" 42 | web_folder="${export_folder}/${rel_web_folder}" 43 | 44 | # Cleanup p8 folder in case old extra cartridges remain that would not be overwritten 45 | rm -rf "${p8_folder}/"* 46 | 47 | # Cleanup png folder as PICO-8 will prompt before overwriting an existing cartridge with the same name, 48 | # and we cannot reply "y" to prompt in headless script (and png tends to keep old label when overwritten) 49 | # Note that we prefer deleting folder content than folder, to avoid file browser/terminal sometimes 50 | # continuing to show old folder in system bin. Make sure to place blob * outside "" 51 | rm -rf "${png_folder}/"* 52 | 53 | # Cleanup bin folder as we want to remove any extraneous files too 54 | # Note: PICO-8 used to accumulate files in .zip for each export (even homonymous files!), 55 | # making this mandatory, but this was fixed in 0.2.2. We still decided to keep the line to be clean. 56 | rm -rf "${bin_folder}/"* 57 | 58 | # p8 cartridges can be distributed as such, so just copy them to the folder to zip later 59 | mkdir -p "$p8_folder" 60 | cp "${export_folder}/"*.p8 "$p8_folder" 61 | 62 | # Create a variant of each non-data cartridge for PNG export, that reloads .p8.png instead of .p8 63 | adapt_for_png_cmd="python3.6 \"$picoboots_scripts_path/adapt_for_png.py\" "${export_folder}/${cartridge_stem}_*.p8 64 | echo "> $adapt_for_png_cmd" 65 | bash -c "$adapt_for_png_cmd" 66 | 67 | if [[ $? -ne 0 ]]; then 68 | echo "" 69 | echo "Adapt for PNG step failed, STOP." 70 | exit 1 71 | fi 72 | 73 | # Copy custom template to PICO-8 config plates folder as "${cartridge_basename}_template.html" 74 | # (just to avoid conflicts with other games) 75 | cp "${picoboots_plates_path}/custom_template.html" "${config_plates_dirpath}/${cartridge_stem}_template.html" 76 | 77 | # Export via PICO-8 editor: PNG cartridges, binaries, HTML 78 | /Applications/PICO-8.app/Contents/MacOS/pico8 -x "$game_scripts_path/export_game_release.p8" 79 | 80 | if [[ $? -ne 0 ]]; then 81 | echo "" 82 | echo "Export game release via PICO-8 step failed, STOP." 83 | exit 1 84 | fi 85 | 86 | # vs_qpu is the biggest cartridge so if PNG export fails, this one will fail first 87 | # if [[ ! -f "${png_folder}/quantattack_vs_qpu.p8.png" ]]; then 88 | # echo "" 89 | # echo "Exporting PNG cartridge for vs_qpu via PICO-8 failed, STOP. Check that this cartridge compressed size <= 100% even after adding '.png' for reload." 90 | # exit 1 91 | # fi 92 | 93 | # if [[ ! -d "$bin_folder" || ! $(ls -A "$bin_folder") ]]; then 94 | # echo "" 95 | # echo "Exporting game release binaries via PICO-8 failed, STOP. Check that each cartridge compressed size <= 100%." 96 | # exit 1 97 | # fi 98 | 99 | # Rename HTML file to index.html for direct play-in-browser 100 | html_filepath="${web_folder}/${cartridge_basename}.html" 101 | mv "$html_filepath" "${web_folder}/index.html" 102 | 103 | # Archiving 104 | # The archives we create here keep all the files under a folder with the full game name 105 | # to avoid extracting files "in the wild". They are meant for manual distribution and preservation. 106 | # itch.io uses a diff system with butler to only upload minimal patches, but surprisingly works well 107 | # with folder structure changing slightly (renaming containing folder and executable), so don't worry 108 | # about providing those customized zip archives to butler. 109 | # Note that for OSX, the .app folder is at the same time the app and the top-level element. 110 | # pushd "${export_folder}" 111 | 112 | # # P8 cartridges archive (delete existing one to be safe) 113 | # rm -f "${cartridge_basename}_cartridges.zip" 114 | # zip -r "${cartridge_basename}_cartridges.zip" "$rel_p8_folder" 115 | 116 | # # PNG cartridges archive (delete existing one to be safe) 117 | # rm -f "${cartridge_basename}_png_cartridges.zip" 118 | # zip -r "${cartridge_basename}_png_cartridges.zip" "$rel_png_folder" 119 | 120 | # # PNG cartridges archive (delete existing one to be safe) 121 | # rm -f "${cartridge_basename}_png_cartridges.zip" 122 | # zip -r "${cartridge_basename}_png_cartridges.zip" "$rel_png_folder" 123 | 124 | # # HTML archive (delete existing one to be safe) 125 | # rm -f "${cartridge_basename}_web.zip" 126 | # zip -r "${cartridge_basename}_web.zip" "${cartridge_basename}_web" 127 | 128 | # # Bin archives 129 | # pushd "${rel_bin_folder}" 130 | 131 | # # Linux archive 132 | 133 | # # Rename linux folder with full game name so our archive contains a self-explanatory folder () 134 | # mv "linux" "${cartridge_basename}_linux" 135 | 136 | # # To minimize operations, do not recreate the archive, just replace the executable in the archive generated by PICO-8 export 137 | # # with the patched executable. We still get some warnings about "Local Version Needed To Extract does not match CD" 138 | # # on other files, so make the operation quiet (-q) 139 | # zip -q "${cartridge_basename}_linux.zip" "${cartridge_basename}_linux/${cartridge_basename}" 140 | 141 | 142 | # # OSX archive 143 | 144 | # # Replace the executable in the archive generated by PICO-8 export with the patched executable 145 | # zip -q "${cartridge_basename}_osx.zip" "${cartridge_basename}.app/Contents/MacOS/${cartridge_basename}" 146 | 147 | 148 | # # Windows archive 149 | 150 | # # Rename linux folder with full game name so our archive contains a self-explanatory folder as the initial archive 151 | # mv "windows" "${cartridge_basename}_windows" 152 | 153 | # # To minimize operations, do not recreate the archive, just replace the executable in the archive generated by PICO-8 export 154 | # # with the patched executable. We still get some warnings about "Local Version Needed To Extract does not match CD" 155 | # # on other files, so make the operation quiet (-q) 156 | # zip -q "${cartridge_basename}_windows.zip" "${cartridge_basename}_windows/${cartridge_basename}.exe" 157 | 158 | # popd 159 | 160 | # popd 161 | -------------------------------------------------------------------------------- /src/lib/qpu.lua: -------------------------------------------------------------------------------- 1 | ---@diagnostic disable: lowercase-global, global-in-nil-env 2 | 3 | function is_single_block(_ENV) 4 | return type == 'h' or type == 'x' or type == 'y' or type == 'z' or type == 's' or type == 't' 5 | end 6 | 7 | local function _is_empty(board, block_x, block_y) 8 | if block_x < 1 or board.cols < block_x or board.rows < block_y then 9 | return false 10 | end 11 | 12 | return board.blocks[block_y][block_x].state == "idle" and board:is_empty(block_x, block_y) 13 | end 14 | 15 | local function _is_match(board, block_x, block_y, block) 16 | if board.rows < block_y then 17 | return false 18 | end 19 | 20 | local other_block = board.blocks[block_y][block_x] 21 | return other_block.type == block.type and other_block.state == "idle" 22 | end 23 | 24 | local function _is_swappable(board, block_x, block_y) 25 | if block_x < 1 or board.cols < block_x then 26 | return false 27 | end 28 | 29 | local block = board.blocks[block_y][block_x] 30 | return block.state == "idle" and (board:is_empty(block_x, block_y) or is_single_block(block)) 31 | end 32 | 33 | -- singleton 34 | qpu_class = new_class() 35 | 36 | -- 新しい QPU プレーヤーを返す 37 | -- 38 | -- level 3: easy, 2: normal, 1: hard 39 | function qpu_class._init(_ENV, _board, _level) 40 | -- raise はテスト用で、false にすると QPU プレーヤーは x を押さない 41 | -- 通常は常に true 42 | board, cursor, level, sleep, raise = 43 | _board, _board and _board.cursor or nil, _level or 2, true, true 44 | init(_ENV) 45 | end 46 | 47 | function qpu_class.init(_ENV) 48 | score, commands = 0, {} 49 | end 50 | 51 | function qpu_class.update(_ENV) 52 | left, right, up, down, x, o, next_command = false, false, false, false, false, false, commands[1] 53 | 54 | if next_command then 55 | del(commands, next_command) 56 | _ENV[next_command] = true 57 | else 58 | if raise and board.top_block_y < 7 then 59 | add(commands, "o") 60 | add_sleep_command(_ENV, 3) 61 | else 62 | return for_all_reducible_blocks(_ENV, _reduce_cnot) or 63 | for_all_reducible_blocks(_ENV, _flatten_block) or 64 | board.contains_q_block or 65 | for_all_reducible_blocks(_ENV, _reduce_single_block) 66 | end 67 | end 68 | end 69 | 70 | function qpu_class._flatten_block(_ENV, each, each_x, each_y) 71 | -- 二列目より上のブロックについて、空のブロックがあればそこに落とす 72 | if 1 < each_y and is_single_block(each) then 73 | if find_left_and_right(_ENV, _is_empty, each, false, true) then 74 | return true 75 | end 76 | end 77 | end 78 | 79 | function qpu_class._reduce_single_block(_ENV, each, each_x, each_y) 80 | if is_single_block(each) then 81 | -- 下の行とマッチするか走査 82 | if each_y > 1 then 83 | if find_left_and_right(_ENV, _is_match, each) then 84 | return true 85 | end 86 | end 87 | 88 | -- 上の行とマッチするか走査 89 | if each_y < board.rows then 90 | if find_left_and_right(_ENV, _is_match, each, true) then 91 | return true 92 | end 93 | end 94 | end 95 | end 96 | 97 | function qpu_class._reduce_cnot(_ENV, each, each_x, each_y) 98 | local upper_block, lower_block = 99 | each_y < board.rows and board:reducible_block_at(each_x, each_y + 1) or block_class("i"), 100 | each_y > 1 and board:reducible_block_at(each_x, each_y - 1) or block_class("i") 101 | 102 | if not is_single_block(each) then 103 | -- d-2. 上の X-C を左にずらす 104 | -- 105 | -- [X--]-C 106 | -- X-C ■ 107 | if each.type == "cnot_x" and each.other_x == each_x + 2 and 108 | lower_block.type == "cnot_x" and 109 | lower_block.other_x == each_x + 1 then 110 | move_and_swap(_ENV, each_x + 1, each_y) 111 | return true 112 | end 113 | 114 | -- e-2. 下の X-C を左にずらす 115 | -- 116 | -- X-C ■ 117 | -- [X--]-C 118 | if each.type == "cnot_x" and each.other_x == each_x + 2 and 119 | upper_block.type == "cnot_x" and 120 | upper_block.other_x == each_x + 1 then 121 | move_and_swap(_ENV, each_x + 1, each_y) 122 | return true 123 | end 124 | 125 | -- a. CNOT を縮める 126 | -- 127 | -- [X-]--C 128 | -- [C-]--X 129 | if (each.type == "cnot_x" or each.type == "control") and each_x + 1 < each.other_x then 130 | move_and_swap(_ENV, each_x, each_y) 131 | return true 132 | end 133 | 134 | -- b. CNOT を同じ方向 (右がC) にそろえる 135 | -- 136 | -- C-X --> X-C 137 | -- X-C --> X-C 138 | if each.type == "control" and each.other_x == each_x + 1 then 139 | move_and_swap(_ENV, each_x, each_y) 140 | return true 141 | end 142 | 143 | -- c. CNOT を右に移動 144 | -- 145 | -- X-[C ] 146 | if each_x < board.cols and 147 | each.type == "control" and each.other_x < each_x and _is_empty(board, each_x + 1, each_y) then 148 | move_and_swap(_ENV, each_x, each_y) 149 | return true 150 | end 151 | 152 | -- d-1. 上の X-C を左にずらす 153 | -- 154 | -- [ X]-C 155 | -- X-C ■ 156 | if each_x > 1 and each_y > 1 and 157 | _is_empty(board, each_x - 1, each_y) and each.type == "cnot_x" and each.other_x == each_x + 1 and 158 | lower_block.type == "control" and 159 | lower_block.other_x == each_x - 1 then 160 | move_and_swap(_ENV, each_x - 1, each_y) 161 | return true 162 | end 163 | 164 | -- e. 下の X-C を左にずらす 165 | -- 166 | -- X-C ■ 167 | -- [ X]-C 168 | if each_x > 1 and 169 | _is_empty(board, each_x - 1, each_y) and each.type == "cnot_x" and each.other_x == each_x + 1 and 170 | upper_block.type == "control" and 171 | upper_block.other_x == each_x - 1 then 172 | move_and_swap(_ENV, each_x - 1, each_y) 173 | return true 174 | end 175 | end 176 | end 177 | 178 | function qpu_class.find_left_and_right(_ENV, f, block, upper) 179 | local block_x, block_y, other_row_block_y, find_left, find_right = 180 | block.x, block.y, block.y + (upper and 1 or -1), true, true 181 | 182 | for dx = 1, board.cols - 1 do 183 | if not (find_left or find_right) then 184 | return false 185 | end 186 | 187 | if find_left then 188 | if _is_swappable(board, block_x - dx, block_y) then 189 | if f(board, block_x - dx, other_row_block_y, block) then 190 | move_and_swap(_ENV, block_x - 1, block_y) 191 | return true 192 | end 193 | else 194 | find_left = false 195 | end 196 | end 197 | 198 | if find_right then 199 | if _is_empty(board, block_x + dx, block_y) then 200 | if f(board, block_x + dx, other_row_block_y, block) then 201 | move_and_swap(_ENV, block_x, block_y) 202 | return true 203 | end 204 | else 205 | find_right = false 206 | end 207 | end 208 | end 209 | 210 | return false 211 | end 212 | 213 | function qpu_class.move_and_swap(_ENV, block_x, block_y) 214 | add_move_command(_ENV, block_x < cursor.x and "left" or "right", abs(cursor.x - block_x)) 215 | add_move_command(_ENV, block_y < cursor.y and "down" or "up", abs(cursor.y - block_y)) 216 | add_swap_command(_ENV) 217 | end 218 | 219 | function qpu_class.add_move_command(_ENV, direction, count) 220 | for _ = 1, count do 221 | add(commands, direction) 222 | 223 | if sleep then 224 | add_sleep_command(_ENV, ceil_rnd(level * 8)) 225 | end 226 | end 227 | end 228 | 229 | function qpu_class.add_swap_command(_ENV) 230 | add(commands, "x") 231 | -- NOTE: ブロックの入れ替えコマンドを送った後は、 232 | -- 必ず次のように入れ替え完了するまで sleep する。 233 | -- これをしないと「左に連続して移動して落とす」などの 234 | -- 操作がうまく行かない。 235 | add_sleep_command(_ENV, 3) 236 | end 237 | 238 | function qpu_class.add_sleep_command(_ENV, count) 239 | for _ = 1, count do 240 | add(commands, "sleep") 241 | end 242 | end 243 | 244 | function qpu_class.for_all_reducible_blocks(_ENV, f) 245 | for each_y = 12, 1, -1 do 246 | for each_x = 1, board.cols do 247 | local each = board.reducible_blocks[each_y][each_x] 248 | if each and f(_ENV, each, each_x, each_y) then 249 | return true 250 | end 251 | end 252 | end 253 | 254 | return false 255 | end 256 | -------------------------------------------------------------------------------- /test/block_fall_utest.lua: -------------------------------------------------------------------------------- 1 | require("engine/test/bustedhelper") 2 | require("engine/render/color") 3 | require("test/test_helper") 4 | require("lib/effects") 5 | require("lib/board") 6 | 7 | describe('ブロックの落下', function() 8 | local board 9 | 10 | before_each(function() 11 | board = board_class() 12 | end) 13 | 14 | describe('ブロックが 1 つだけ落ちる', function() 15 | local block 16 | 17 | before_each(function() 18 | block = block_class("h") 19 | end) 20 | 21 | it("状態が hover になる", function() 22 | board:put(1, 2, block) 23 | 24 | board:update() 25 | 26 | assert.is_true(block.state == "hover") 27 | end) 28 | 29 | it("1 フレームで 1 ブロック落下する", function() 30 | board:put(1, 2, block) 31 | 32 | -- hover 状態に遷移 33 | board:update() 34 | 35 | for i = 1, 12 do 36 | board:update() 37 | end 38 | 39 | -- fall 状態に遷移 40 | board:update() 41 | 42 | assert.is_true(block.state == "fall") 43 | assert.are.equal("h", board:block_at(1, 1).type) 44 | end) 45 | 46 | it("着地後 1 フレームで状態が idle になる", function() 47 | board:put(1, 2, block) 48 | 49 | board:update() 50 | 51 | for i = 1, 12 do 52 | board:update() 53 | end 54 | 55 | board:update() 56 | board:update() 57 | 58 | assert.is_true(block.state == "idle") 59 | end) 60 | end) 61 | 62 | describe('ブロックが 2 つ積み重なったまま落ちる', function() 63 | local block1, block2 64 | 65 | before_each(function() 66 | block1 = block_class("h") 67 | block2 = block_class("x") 68 | end) 69 | 70 | it("状態が hover になる", function() 71 | board:put(1, 3, block1) 72 | board:put(1, 2, block2) 73 | 74 | board:update() 75 | 76 | assert.is_true(block1.state == "hover") 77 | assert.is_true(block2.state == "hover") 78 | end) 79 | 80 | it("1 フレームで 1 ブロック落下する", function() 81 | board:put(1, 3, block1) 82 | board:put(1, 2, block2) 83 | 84 | -- hover 状態に遷移 85 | board:update() 86 | 87 | -- hover が 12 フレーム続く 88 | assert.is_true(board:block_at(1, 3).state == "hover") 89 | assert.is_true(board:block_at(1, 2).state == "hover") 90 | 91 | for i = 1, 12 do 92 | board:update() 93 | 94 | assert.is_true(board:block_at(1, 3).state == "hover") 95 | assert.is_true(board:block_at(1, 2).state == "hover") 96 | end 97 | 98 | -- fall 状態に遷移 99 | board:update() 100 | 101 | assert.is_true(board:block_at(1, 2).state == "fall") 102 | assert.is_true(board:block_at(1, 1).state == "fall") 103 | 104 | assert.are.equal("h", board:block_at(1, 2).type) 105 | assert.are.equal("x", board:block_at(1, 1).type) 106 | end) 107 | 108 | it("着地後 1 フレームで状態が idle になる", function() 109 | board:put(1, 3, block1) 110 | board:put(1, 2, block2) 111 | 112 | board:update() 113 | 114 | for i = 1, 12 do 115 | board:update() 116 | end 117 | 118 | board:update() 119 | board:update() 120 | 121 | assert.is_true(block1.state == "idle") 122 | assert.is_true(block2.state == "idle") 123 | end) 124 | end) 125 | 126 | describe('CNOT が落ちる', function() 127 | local control, cnot_x 128 | 129 | -- C-X 130 | before_each(function() 131 | control = control_block(2) 132 | cnot_x = cnot_x_block(1) 133 | end) 134 | 135 | it("状態が fall になる", function() 136 | board:put(1, 2, control) 137 | board:put(2, 2, cnot_x) 138 | 139 | board:update() 140 | 141 | for i = 1, 12 do 142 | board:update() 143 | end 144 | 145 | board:update() 146 | 147 | assert.is_true(control.state == "fall") 148 | assert.is_true(cnot_x.state == "fall") 149 | end) 150 | 151 | it("1 フレームで 1 ブロック落下する", function() 152 | board:put(1, 2, control) 153 | board:put(2, 2, cnot_x) 154 | 155 | board:update() 156 | 157 | for i = 1, 12 do 158 | board:update() 159 | end 160 | 161 | board:update() 162 | 163 | assert.is_true(control.state == "fall") 164 | assert.is_true(cnot_x.state == "fall") 165 | assert.are.equal(control, board:block_at(1, 1)) 166 | assert.are.equal(cnot_x, board:block_at(2, 1)) 167 | end) 168 | 169 | it("着地後 1 フレームで状態が idle になる", function() 170 | board:put(1, 2, control) 171 | board:put(2, 2, cnot_x) 172 | 173 | board:update() 174 | 175 | for i = 1, 12 do 176 | board:update() 177 | end 178 | 179 | board:update() 180 | board:update() 181 | 182 | assert.is_true(control.state == "idle") 183 | assert.is_true(cnot_x.state == "idle") 184 | end) 185 | end) 186 | 187 | describe('SWAP ブロックが落ちる', function() 188 | local swap_left, swap_right 189 | 190 | before_each(function() 191 | swap_left = swap_block(2) 192 | swap_right = swap_block(1) 193 | end) 194 | 195 | it("状態が fall になる", function() 196 | board:put(1, 2, swap_left) 197 | board:put(2, 2, swap_right) 198 | 199 | board:update() 200 | 201 | for i = 1, 12 do 202 | board:update() 203 | end 204 | 205 | board:update() 206 | 207 | assert.is_true(swap_left.state == "fall") 208 | assert.is_true(swap_right.state == "fall") 209 | end) 210 | 211 | it("1 フレームで 1 ブロック落下する", function() 212 | board:put(1, 2, swap_left) 213 | board:put(2, 2, swap_right) 214 | 215 | board:update() 216 | 217 | for i = 1, 12 do 218 | board:update() 219 | end 220 | 221 | board:update() 222 | 223 | assert.is_true(swap_left.state == "fall") 224 | assert.is_true(swap_right.state == "fall") 225 | assert.are.equal(swap_left, board:block_at(1, 1)) 226 | assert.are.equal(swap_right, board:block_at(2, 1)) 227 | end) 228 | 229 | it("着地後 1 フレームで状態が idle になる", function() 230 | board:put(1, 2, swap_left) 231 | board:put(2, 2, swap_right) 232 | 233 | board:update() 234 | 235 | for i = 1, 12 do 236 | board:update() 237 | end 238 | 239 | board:update() 240 | board:update() 241 | 242 | assert.is_true(swap_left.state == "idle") 243 | assert.is_true(swap_right.state == "idle") 244 | end) 245 | 246 | it('下のブロックをずらして落とす', function() 247 | -- 248 | -- S-S ---> S-S ---> 249 | -- H H S-S H 250 | board:put(1, 2, swap_left) 251 | board:put(2, 2, swap_right) 252 | board:put(2, 1, block_class("h")) 253 | 254 | board:swap(2, 1) 255 | 256 | -- swap が 3 フレーム 257 | board:update() 258 | board:update() 259 | board:update() 260 | 261 | board:update() 262 | 263 | -- hover 状態 264 | for i = 1, 12 do 265 | board:update() 266 | end 267 | 268 | board:update() 269 | board:update() 270 | 271 | assert.are.equal(swap_left, board:block_at(1, 1)) 272 | assert.are.equal(swap_right, board:block_at(2, 1)) 273 | assert.is_true(swap_left.state == "idle") 274 | assert.is_true(swap_right.state == "idle") 275 | end) 276 | end) 277 | 278 | describe('おじゃまブロックが落ちる', function() 279 | local garbage 280 | 281 | before_each(function() 282 | garbage = garbage_block(3, 3) 283 | end) 284 | 285 | it("状態が fall になる", function() 286 | board:put(1, 2, garbage) 287 | 288 | board:update() 289 | 290 | for i = 1, 12 do 291 | board:update() 292 | end 293 | 294 | board:update() 295 | 296 | assert.is_true(garbage.state == "fall") 297 | end) 298 | 299 | it("1 フレームで 1 ブロック落下する", function() 300 | board:put(1, 2, garbage) 301 | 302 | board:update() 303 | 304 | for i = 1, 12 do 305 | board:update() 306 | end 307 | 308 | board:update() 309 | 310 | assert.is_true(garbage.state == "fall") 311 | assert.are.equal(garbage, board:block_at(1, 1)) 312 | end) 313 | 314 | it("着地後 1 フレームで状態が idle になる", function() 315 | board:put(1, 2, garbage) 316 | 317 | board:update() 318 | 319 | for i = 1, 12 do 320 | board:update() 321 | end 322 | 323 | board:update() 324 | board:update() 325 | 326 | assert.is_true(garbage.state == "idle") 327 | end) 328 | end) 329 | end) 330 | -------------------------------------------------------------------------------- /icon.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/rush/game.lua: -------------------------------------------------------------------------------- 1 | local pending_garbage_blocks = pending_garbage_blocks_class() 2 | local game = new_class() 3 | 4 | local all_players_info, countdown 5 | 6 | local chain_bonus = transform(split("0,50,80,150,300,400,500,700,900,1100,1300,1500,1800"), tonum) 7 | 8 | function game.reduce_callback(score, player, board, contains_swap) 9 | local score_delta = score >> 16 10 | player.score = player.score + score_delta 11 | 12 | if contains_swap then 13 | board.freeze_timer = 120 14 | end 15 | end 16 | 17 | function game.combo_callback(combo_count, coord, player, board, other_board) 18 | bubbles:create("combo", combo_count, coord) 19 | ions:create( 20 | coord, 21 | board.attack_ion_target, 22 | function(target) 23 | sfx(21) 24 | particles:create( 25 | target, 26 | "5,5,9,7,,,-0.03,-0.03,20|5,5,9,7,,,-0.03,-0.03,20|4,4,9,7,,,-0.03,-0.03,20|4,4,2,5,,,-0.03,-0.03,20|4,4,6,7,,,-0.03,-0.03,20|2,2,9,7,,,-0.03,-0.03,20|2,2,9,7,,,-0.03,-0.03,20|2,2,6,5,,,-0.03,-0.03,20|2,2,6,5,,,-0.03,-0.03,20|0,0,2,5,,,-0.03,-0.03,20" 27 | ) 28 | 29 | local score = combo_count * 10 30 | local score_delta = score >> 16 31 | player.score = player.score + score_delta 32 | 33 | -- 対戦相手がいる時、おじゃまブロックを送る 34 | if other_board then 35 | other_board:send_garbage(nil, combo_count > 6 and 6 or combo_count - 1, 1) 36 | end 37 | end 38 | ) 39 | end 40 | 41 | function game.block_offset_callback(chain_count, coord, player, board, other_board) 42 | local offset_height = chain_count 43 | 44 | if offset_height > 2 then 45 | ions:create( 46 | coord, 47 | board.block_offset_target, 48 | function(target) 49 | sfx(21) 50 | particles:create( 51 | target, 52 | "5,5,9,7,,,-0.03,-0.03,20|5,5,9,7,,,-0.03,-0.03,20|4,4,9,7,,,-0.03,-0.03,20|4,4,2,5,,,-0.03,-0.03,20|4,4,6,7,,,-0.03,-0.03,20|2,2,9,7,,,-0.03,-0.03,20|2,2,9,7,,,-0.03,-0.03,20|2,2,6,5,,,-0.03,-0.03,20|2,2,6,5,,,-0.03,-0.03,20|0,0,2,5,,,-0.03,-0.03,20" 53 | ) 54 | 55 | local score = chain_bonus[chain_count] or 1800 56 | local score_delta = score >> 16 57 | player.score = player.score + score_delta 58 | 59 | if other_board then 60 | offset_height = pending_garbage_blocks:offset(offset_height) 61 | end 62 | end, 63 | 9 64 | ) 65 | end 66 | 67 | return offset_height 68 | end 69 | 70 | function game.chain_callback(chain_id, chain_count, coord, player, board, other_board) 71 | if chain_count > 1 then 72 | bubbles:create("chain", chain_count, coord) 73 | ions:create( 74 | coord, 75 | board.attack_ion_target, 76 | function(target) 77 | sfx(21) 78 | particles:create( 79 | target, 80 | "5,5,9,7,,,-0.03,-0.03,20|5,5,9,7,,,-0.03,-0.03,20|4,4,9,7,,,-0.03,-0.03,20|4,4,2,5,,,-0.03,-0.03,20|4,4,6,7,,,-0.03,-0.03,20|2,2,9,7,,,-0.03,-0.03,20|2,2,9,7,,,-0.03,-0.03,20|2,2,6,5,,,-0.03,-0.03,20|2,2,6,5,,,-0.03,-0.03,20|0,0,2,5,,,-0.03,-0.03,20" 81 | ) 82 | 83 | local score = chain_bonus[chain_count] or 1800 84 | local score_delta = score >> 16 85 | player.score = player.score + score_delta 86 | 87 | -- 対戦相手がいる時、おじゃまブロックを送る 88 | if other_board then 89 | other_board:send_garbage(chain_id, 6, chain_count - 1 < 6 and chain_count - 1 or 5) 90 | end 91 | end 92 | ) 93 | else 94 | local score = chain_bonus[chain_count] or 1800 95 | local score_delta = score >> 16 96 | player.score = player.score + score_delta 97 | end 98 | end 99 | 100 | function game:is_game_over() 101 | return self.game_over_time ~= nil 102 | end 103 | 104 | function game:_init() 105 | self.auto_raise_frame_count = 10 106 | end 107 | 108 | function game:init() 109 | all_players_info = {} 110 | countdown = 240 111 | self.start_time = t() 112 | self.game_over_time = nil 113 | self.time_left = nil 114 | 115 | music(-1) -- stop the music 116 | end 117 | 118 | function game:add_player(player, board, other_board) 119 | add(all_players_info, { 120 | player = player, 121 | board = board, 122 | other_board = other_board, 123 | tick = 0 124 | }) 125 | end 126 | 127 | function game:update() 128 | ripple:update_all() 129 | 130 | if countdown then 131 | countdown = countdown - 1 132 | local countdown_number = flr(countdown / 60 + 1) 133 | 134 | if countdown > 0 then 135 | self.start_time = t() 136 | 137 | if countdown_number < 4 then 138 | for _, each in pairs(all_players_info) do 139 | each.board.countdown = countdown_number 140 | end 141 | end 142 | 143 | if countdown % 60 == 0 then 144 | sfx(13) 145 | end 146 | elseif countdown == 0 then 147 | countdown = nil 148 | 149 | for _, each in pairs(all_players_info) do 150 | each.board.countdown = nil 151 | end 152 | 153 | sfx(14) 154 | end 155 | end 156 | 157 | if not countdown and -- カウントダウン終了 158 | stat(46) == -1 and -- カウントダウンの sfx が鳴り終わっている 159 | stat(54) == -1 then -- BGM がまだ始まっていない 160 | music(0) 161 | end 162 | 163 | local music_pattern_id = stat(54) 164 | if music_pattern_id then 165 | if all_players_info[1].board:is_topped_out() then 166 | if 0 <= music_pattern_id and music_pattern_id <= 7 then 167 | music(16) 168 | end 169 | else 170 | if 16 <= music_pattern_id and music_pattern_id <= 19 then 171 | music(0) 172 | end 173 | end 174 | end 175 | 176 | -- もしどちらかの board でおじゃまブロックを分解中だった場合 "slow" にする 177 | ripple.slow = false 178 | 179 | for index, each in pairs(all_players_info) do 180 | local player = each.player 181 | local board = each.board 182 | local cursor = board.cursor 183 | local other_board = each.other_board 184 | 185 | if self:is_game_over() then 186 | board:update() 187 | ripple.slow = false 188 | else 189 | player:update(board) 190 | 191 | if player.left then 192 | sfx(8) 193 | cursor:move_left() 194 | end 195 | if player.right then 196 | sfx(8) 197 | cursor:move_right(board.cols) 198 | end 199 | if player.up then 200 | sfx(8) 201 | cursor:move_up(board.rows) 202 | end 203 | if player.down then 204 | sfx(8) 205 | cursor:move_down() 206 | end 207 | if player.x and not countdown and board:swap(cursor.x, cursor.y) then 208 | sfx(10) 209 | end 210 | if player.o and not countdown and board.top_block_y > 2 then 211 | self:_raise(each) 212 | end 213 | 214 | board:update(self, player, other_board) 215 | cursor:update() 216 | 217 | if not countdown then 218 | self:_auto_raise(each) 219 | end 220 | 221 | if board.contains_q_block then 222 | ripple.slow = true 223 | end 224 | end 225 | end 226 | 227 | particles:update_all() 228 | bubbles:update_all() 229 | ions:update_all() 230 | 231 | if all_players_info[1].board:is_game_over() then 232 | if self.game_over_time == nil then 233 | self.game_over_time = t() 234 | end 235 | else 236 | -- ゲーム中だけ time_left を更新 237 | game.time_left = 120 - (t() - self.start_time) 238 | end 239 | end 240 | 241 | function game:render() -- override 242 | ripple:render_all() 243 | 244 | for _, each in pairs(all_players_info) do 245 | local board = each.board 246 | 247 | board:render() 248 | 249 | -- カウントダウンの数字はカーソルの上に表示 250 | if board.countdown then 251 | local countdown_sprite_x = { 32, 16, 0 } 252 | sspr(countdown_sprite_x[board.countdown], 80, 253 | 16, 16, 254 | board.offset_x + 16, 43) 255 | end 256 | end 257 | 258 | particles:render_all() 259 | bubbles:render_all() 260 | ions:render_all() 261 | end 262 | 263 | -- ブロックをせりあげる 264 | function game:_raise(player_info, force) 265 | local board = player_info.board 266 | 267 | if not board:is_topped_out() or force then 268 | board.raised_dots = board.raised_dots + 1 269 | 270 | if board.raised_dots == 8 then 271 | board.raised_dots = 0 272 | board:insert_blocks_at_bottom() 273 | board.cursor:move_up(board.rows) 274 | end 275 | end 276 | end 277 | 278 | -- 可能な場合ブロックを自動的にせりあげる 279 | function game:_auto_raise(player_info) 280 | if player_info.board.freeze_timer > 0 or player_info.board:is_busy() then 281 | return 282 | end 283 | 284 | player_info.tick = player_info.tick + 1 285 | 286 | if player_info.tick > self.auto_raise_frame_count then 287 | self:_raise(player_info, true) 288 | player_info.tick = 0 289 | end 290 | end 291 | 292 | -- 残り時間を文字列で返す (e.g., "01:23") 293 | function game:time_left_string() 294 | return length2_number_string_0filled(flr(self.time_left / 60)) .. 295 | ":" .. 296 | length2_number_string_0filled(ceil(self.time_left) % 60) 297 | end 298 | 299 | function length2_number_string_0filled(num) 300 | return (num < 10) and "0" .. num or num 301 | end 302 | 303 | return game 304 | -------------------------------------------------------------------------------- /src/lib/block.lua: -------------------------------------------------------------------------------- 1 | ---@diagnostic disable: lowercase-global 2 | 3 | --- ブロック (量子ゲート) クラス 4 | block_class = new_class() 5 | block_class.block_match_animation_frame_count = 45 6 | block_class.block_match_delay_per_block = 8 7 | block_class.block_swap_animation_frame_count = 3 8 | block_class.sprites = transform({ 9 | -- default|landing|match 10 | h = "0|1,1,1,2,2,2,3,3,1,1,1,1|24,24,24,25,25,25,24,24,24,26,26,26,0,0,0,27", 11 | x = "16|17,17,17,18,18,18,19,19,17,17,17,17|40,40,40,41,41,41,40,40,40,42,42,42,16,16,16,43", 12 | y = "32|33,33,33,34,34,34,35,35,33,33,33,33|56,56,56,57,57,57,56,56,56,58,58,58,32,32,32,59", 13 | z = "48|49,49,49,50,50,50,51,51,49,49,49,49|12,12,12,13,13,13,12,12,12,14,14,14,48,48,48,15", 14 | s = "4|5,5,5,6,6,6,7,7,5,5,5,5|28,28,28,29,29,29,28,28,28,30,30,30,4,4,4,31", 15 | t = "20|21,21,21,22,22,22,23,23,21,21,21,21|44,44,44,45,45,45,44,44,44,46,46,46,20,20,20,47", 16 | control = "36|37,37,37,38,38,38,39,39,37,37,37,37|60,60,60,61,61,61,60,60,60,62,62,62,36,36,36,63", 17 | cnot_x = "52|53,53,53,54,54,54,55,55,53,53,53,53|64,64,64,65,65,65,64,64,64,66,66,66,52,52,52,67", 18 | swap = "8|9,9,9,10,10,10,11,11,9,9,9,9|80,80,80,81,81,81,80,80,80,82,82,82,8,8,8,83", 19 | ["?"] = "98|98,98,98,98,98,98,98,98,98,98,98,98|98,98,98,98,98,98,98,98,98,98,98,98,98,98,98,98", 20 | ["#"] = "113|113,113,113,113,113,113,113,113,113,113,113,113|113,113,113,113,113,113,113,113,113,113,113,113,113,113,113,113" 21 | }, function(each) 22 | local default, landing, match = unpack(split(each, "|")) 23 | return { 24 | default = default, 25 | landing = split(landing), 26 | match = split(match) 27 | } 28 | end) 29 | 30 | -- TODO: span, height は garbage の特異プロパティにする? 検討 31 | -- TODO: _timer_landing の初期化いる? 検討 32 | function block_class._init(_ENV, _type, _span, _height) 33 | type, state, span, height, sprite_set, _timer_landing = 34 | _type, "idle", _span or 1, _height or 1, sprites[_type], 0 35 | end 36 | 37 | -- NOTE: board クラスでは「ブロックが落とせない状態か?」という判定しか行わないため、 38 | -- not block:is_fallable() ではなく block:is_not_fallable() と書けるようにする。 39 | -- これによって "not" のトークンを削減 40 | function block_class.is_not_fallable(_ENV) 41 | return type == "i" or type == "?" or state == "swap" or state == "freeze" or state == "match" 42 | end 43 | 44 | function block_class.is_reducible(_ENV) 45 | return state == "idle" and type ~= "i" and type ~= "?" 46 | end 47 | 48 | function block_class:is_swappable_state() 49 | return self.state == "idle" or self.state == "fall" 50 | end 51 | 52 | function block_class:swap_with(direction) 53 | self.chain_id = nil 54 | self.swap_direction = direction 55 | self:change_state("swap") 56 | end 57 | 58 | function block_class:hover(timer) 59 | self.timer = timer or 12 -- ホバー状態はデフォルトで 12 フレーム 60 | self:change_state("hover") 61 | end 62 | 63 | function block_class:fall() 64 | --#if assert 65 | assert(not self:is_not_fallable(), "block " .. self.type) 66 | assert(self.x, "x is not set") 67 | assert(self.y, "y is not set") 68 | --#endif 69 | 70 | if self.state == "fall" then 71 | return 72 | end 73 | 74 | self:change_state("fall") 75 | end 76 | 77 | function block_class.replace_with(_ENV, other, match_index, _chain_id, garbage_span, garbage_height) 78 | new_block, _match_index, _tick_match, chain_id, other.chain_id, _garbage_span, _garbage_height = 79 | other, match_index or 0, 1, _chain_id, _chain_id, garbage_span, garbage_height 80 | 81 | change_state(_ENV, "match") 82 | end 83 | 84 | function block_class.update(_ENV) 85 | if state == "idle" then 86 | if _timer_landing > 0 then 87 | _timer_landing = _timer_landing - 1 88 | end 89 | elseif state == "swap" then 90 | if _tick_swap < block_swap_animation_frame_count then 91 | _tick_swap = _tick_swap + 1 92 | else 93 | chain_id = nil 94 | change_state(_ENV, "idle") 95 | end 96 | elseif state == "hover" then 97 | if timer > 0 then 98 | timer = timer - 1 99 | else 100 | change_state(_ENV, "idle") 101 | end 102 | elseif state == "match" then 103 | if _tick_match <= block_match_animation_frame_count + _match_index * block_match_delay_per_block then 104 | _tick_match = _tick_match + 1 105 | else 106 | change_state(_ENV, "idle") 107 | 108 | if _garbage_span then 109 | new_block._tick_freeze = 0 110 | new_block._freeze_frame_count = (_garbage_span * _garbage_height - _match_index) * block_match_delay_per_block 111 | new_block:change_state("freeze") 112 | end 113 | end 114 | elseif state == "freeze" then 115 | if _tick_freeze < _freeze_frame_count then 116 | _tick_freeze = _tick_freeze + 1 117 | else 118 | change_state(_ENV, "idle") 119 | end 120 | end 121 | end 122 | 123 | function block_class:render(screen_x, screen_y, screen_other_x) 124 | local shake_dx, shake_dy, swap_screen_dx, sprite = 0, 0 125 | 126 | do 127 | local _ENV = self 128 | 129 | if type == "i" then 130 | return 131 | end 132 | 133 | swap_screen_dx = (_tick_swap or 0) * (8 / block_swap_animation_frame_count) 134 | if state == "swap" and swap_direction == "left" then 135 | swap_screen_dx = -swap_screen_dx 136 | end 137 | 138 | if state == "idle" and _timer_landing > 0 then 139 | sprite = sprite_set.landing[_timer_landing] 140 | elseif state == "match" then 141 | local sequence = sprite_set.match 142 | sprite = _tick_match <= block_match_delay_per_block and sequence[_tick_match] or sequence[#sequence] 143 | elseif state == "over" then 144 | sprite = sprite_set.match[#sprite_set.match] 145 | else 146 | sprite = sprite_set.default 147 | end 148 | 149 | -- CNOT または SWAP の接続を描画 150 | if other_x and x < other_x then 151 | line( 152 | screen_x + 3, 153 | screen_y + 3, 154 | screen_other_x + 3, 155 | screen_y + 3, 156 | (state == "match") and 13 or 10 157 | ) 158 | end 159 | end 160 | 161 | if self.type == "?" then 162 | palt(0, false) 163 | pal(13, self.body_color) 164 | end 165 | 166 | if self.state == "over" then 167 | shake_dx, shake_dy = rnd(2) - 1, rnd(2) - 1 168 | pal(6, 9) 169 | pal(7, 1) 170 | end 171 | 172 | spr(sprite, screen_x + swap_screen_dx + shake_dx, screen_y + shake_dy) 173 | 174 | palt(0, true) 175 | pal(13, 13) 176 | pal(6, 6) 177 | pal(7, 7) 178 | end 179 | 180 | function block_class:attach(observer) 181 | self.observer = observer 182 | end 183 | 184 | function block_class.change_state(_ENV, new_state) 185 | _timer_landing, _tick_swap = (state == "fall") and 12 or 0, 0 186 | 187 | local old_state = state 188 | state = new_state 189 | 190 | observer:observable_update(_ENV, old_state) 191 | end 192 | 193 | --#if debug 194 | local type_string = { 195 | i = '_', 196 | control = '●', 197 | cnot_x = '+', 198 | swap = 'X' 199 | } 200 | 201 | local state_string = { 202 | idle = " ", 203 | hover = "^", 204 | fall = "|", 205 | match = "*", 206 | freeze = "f", 207 | } 208 | 209 | function block_class:_tostring() 210 | if self.state == "swap" then 211 | if self.swap_direction == "left" then 212 | return (type_string[self.type] or self.type:upper()) .. "<" 213 | elseif self.swap_direction == "right" then 214 | return (type_string[self.type] or self.type:upper()) .. ">" 215 | else 216 | assert(false, "Invalid state") 217 | end 218 | else 219 | return (type_string[self.type] or self.type:upper()) .. state_string[self.state] 220 | end 221 | end 222 | 223 | --#endif 224 | 225 | --- おじゃまブロック 226 | 227 | local garbage_block_colors = { 2, 3, 4 } 228 | local inner_border_colors = { nil, 14, 11, 9 } 229 | 230 | --- 新しいおじゃまブロックを作る 231 | function garbage_block(_span, _height, _color, _chain_id, _tick_fall) 232 | local garbage = setmetatable({ 233 | body_color = _color or garbage_block_colors[ceil_rnd(#garbage_block_colors)], 234 | chain_id = _chain_id, 235 | tick_fall = _tick_fall, 236 | dy = 0, 237 | first_drop = true, 238 | _render_box = draw_rounded_box, 239 | render = function(_ENV, screen_x, screen_y) 240 | local y0, x1, y1, _body_color = 241 | screen_y + (1 - height) * 8, 242 | screen_x + span * 8 - 2, 243 | screen_y + 6, 244 | state ~= "over" and body_color or 9 245 | 246 | _render_box(screen_x, y0 + 1, x1, y1 + 1, 5) -- 影 247 | _render_box(screen_x, y0, x1, y1, _body_color, _body_color) -- 本体 248 | _render_box(screen_x + 1, y0 + 1, x1 - 1, y1 - 1, state ~= "over" and inner_border_color or 1) -- 内側の線 249 | end 250 | }, { __index = block_class("g", _span or 6, _height) }) 251 | 252 | --#if assert 253 | assert(garbage.body_color == 2 or garbage.body_color == 3 or garbage.body_color == 4, 254 | "invalid color: " .. garbage.body_color) 255 | assert(2 < garbage.span, "span must be greater than 2") 256 | assert(garbage.span < 7, "span must be less than 7") 257 | --#endif 258 | 259 | garbage.inner_border_color = inner_border_colors[garbage.body_color] 260 | 261 | return garbage 262 | end 263 | -------------------------------------------------------------------------------- /src/lib/game.lua: -------------------------------------------------------------------------------- 1 | ---@diagnostic disable: global-in-nil-env, lowercase-global 2 | 3 | game_class = new_class() 4 | 5 | local chain_bonus = transform(split("0,50,80,150,300,400,500,700,900,1100,1300,1500,1800"), tonum) 6 | 7 | function game_class.reduce_callback(score, player, board, contains_swap) 8 | local score_delta = score >> 16 9 | player.score = player.score + score_delta 10 | 11 | if contains_swap then 12 | board.freeze_timer = 120 13 | sfx(49) 14 | end 15 | end 16 | 17 | function game_class.combo_callback(combo_count, coord, player, board, other_board) 18 | bubbles:create("combo", combo_count, coord) 19 | ions:create( 20 | coord, 21 | board.attack_ion_target, 22 | function(target) 23 | sfx(21) 24 | particles:create( 25 | target, 26 | "5,5,9,7,,,-0.03,-0.03,20|5,5,9,7,,,-0.03,-0.03,20|4,4,9,7,,,-0.03,-0.03,20|4,4,2,5,,,-0.03,-0.03,20|4,4,6,7,,,-0.03,-0.03,20|2,2,9,7,,,-0.03,-0.03,20|2,2,9,7,,,-0.03,-0.03,20|2,2,6,5,,,-0.03,-0.03,20|2,2,6,5,,,-0.03,-0.03,20|0,0,2,5,,,-0.03,-0.03,20" 27 | ) 28 | 29 | local score = combo_count * 10 30 | local score_delta = score >> 16 31 | player.score = player.score + score_delta 32 | 33 | -- 対戦相手がいる時、おじゃまブロックを送る 34 | if other_board then 35 | other_board:send_garbage(nil, combo_count > 6 and 6 or combo_count - 1, 1) 36 | end 37 | end 38 | ) 39 | end 40 | 41 | function game_class.block_offset_callback(chain_count, coord, player, board, other_board) 42 | local offset_height = chain_count 43 | 44 | if offset_height > 2 then 45 | ions:create( 46 | coord, 47 | board.block_offset_target, 48 | function(target) 49 | sfx(21) 50 | particles:create( 51 | target, 52 | "5,5,9,7,,,-0.03,-0.03,20|5,5,9,7,,,-0.03,-0.03,20|4,4,9,7,,,-0.03,-0.03,20|4,4,2,5,,,-0.03,-0.03,20|4,4,6,7,,,-0.03,-0.03,20|2,2,9,7,,,-0.03,-0.03,20|2,2,9,7,,,-0.03,-0.03,20|2,2,6,5,,,-0.03,-0.03,20|2,2,6,5,,,-0.03,-0.03,20|0,0,2,5,,,-0.03,-0.03,20" 53 | ) 54 | 55 | local score = chain_bonus[chain_count] or 1800 56 | local score_delta = score >> 16 57 | player.score = player.score + score_delta 58 | 59 | if other_board then 60 | offset_height = board.pending_garbage_blocks:offset(offset_height) 61 | end 62 | end, 63 | 9 64 | ) 65 | end 66 | 67 | return offset_height 68 | end 69 | 70 | function game_class.chain_callback(chain_id, chain_count, coord, player, board, other_board) 71 | if chain_count > 1 then 72 | bubbles:create("chain", chain_count, coord) 73 | if chain_count > 2 then 74 | ions:create( 75 | coord, 76 | board.attack_ion_target, 77 | function(target) 78 | sfx(21) 79 | particles:create( 80 | target, 81 | "5,5,9,7,,,-0.03,-0.03,20|5,5,9,7,,,-0.03,-0.03,20|4,4,9,7,,,-0.03,-0.03,20|4,4,2,5,,,-0.03,-0.03,20|4,4,6,7,,,-0.03,-0.03,20|2,2,9,7,,,-0.03,-0.03,20|2,2,9,7,,,-0.03,-0.03,20|2,2,6,5,,,-0.03,-0.03,20|2,2,6,5,,,-0.03,-0.03,20|0,0,2,5,,,-0.03,-0.03,20" 82 | ) 83 | 84 | local score = chain_bonus[chain_count] or 1800 85 | local score_delta = score >> 16 86 | player.score = player.score + score_delta 87 | 88 | -- 対戦相手がいる時、おじゃまブロックを送る 89 | if other_board then 90 | other_board:send_garbage(chain_id, 6, chain_count - 1 < 6 and chain_count - 1 or 5) 91 | end 92 | end 93 | ) 94 | end 95 | else 96 | local score = chain_bonus[chain_count] or 1800 97 | local score_delta = score >> 16 98 | player.score = player.score + score_delta 99 | end 100 | end 101 | 102 | function game_class.is_game_over(_ENV) 103 | return game_over_time ~= nil 104 | end 105 | 106 | function game_class._init(_ENV) 107 | all_players_info, auto_raise_frame_count = {}, 30 108 | end 109 | 110 | function game_class.init(_ENV) 111 | countdown, start_time, game_over_time = 240, t() 112 | 113 | for _, each in pairs(all_players_info) do 114 | each.player:init() 115 | each.board:init() 116 | each.board:put_random_blocks() 117 | end 118 | 119 | music(-1) -- stop the music 120 | end 121 | 122 | function game_class.add_player(_ENV, player, board, other_board) 123 | add( 124 | all_players_info, 125 | { 126 | player = player, 127 | board = board, 128 | other_board = other_board, 129 | tick = 0 130 | } 131 | ) 132 | end 133 | 134 | function game_class.update(_ENV) 135 | ripple:update_all() 136 | 137 | if countdown then 138 | countdown = countdown - 1 139 | local countdown_number = flr(countdown / 60 + 1) 140 | 141 | if countdown > 0 then 142 | start_time = t() 143 | 144 | if countdown_number < 4 then 145 | for _, each in pairs(all_players_info) do 146 | each.board.countdown = countdown_number 147 | end 148 | end 149 | 150 | if countdown % 60 == 0 then 151 | sfx(13) 152 | end 153 | elseif countdown == 0 then 154 | countdown = nil 155 | 156 | for _, each in pairs(all_players_info) do 157 | each.board.countdown = nil 158 | end 159 | 160 | sfx(14) 161 | end 162 | end 163 | 164 | if not countdown and -- カウントダウン終了 165 | stat(46) == -1 and -- カウントダウンの sfx が鳴り終わっている 166 | stat(54) == -1 then -- BGM がまだ始まっていない 167 | music(0) 168 | end 169 | 170 | local music_pattern_id = stat(54) 171 | if music_pattern_id then 172 | if all_players_info[1].board:is_topped_out() then 173 | if 0 <= music_pattern_id and music_pattern_id <= 7 then 174 | music(16) 175 | end 176 | else 177 | if 16 <= music_pattern_id and music_pattern_id <= 19 then 178 | music(0) 179 | end 180 | end 181 | end 182 | 183 | ripple.slow, ripple.freeze = false, false 184 | 185 | for _, each in pairs(all_players_info) do 186 | local player, board, other_board = each.player, each.board, each.other_board 187 | local cursor = board.cursor 188 | 189 | if board:is_game_over() then 190 | board:update() 191 | ripple.slow = false 192 | else 193 | player:update(board) 194 | 195 | if player.left then 196 | sfx(8) 197 | cursor:move_left() 198 | end 199 | if player.right then 200 | sfx(8) 201 | cursor:move_right(board.cols) 202 | end 203 | if player.up then 204 | sfx(8) 205 | cursor:move_up(board.rows) 206 | end 207 | if player.down then 208 | sfx(8) 209 | cursor:move_down() 210 | end 211 | if player.x and not countdown and board:swap(cursor.x, cursor.y) then 212 | sfx(10) 213 | end 214 | if player.o and not countdown and board.top_block_y > 2 then 215 | _raise(_ENV, each) 216 | end 217 | 218 | board:update(_ENV, player, other_board) 219 | cursor:update() 220 | 221 | if not countdown then 222 | _auto_raise(_ENV, each) 223 | end 224 | 225 | -- もしどちらかの board でおじゃまブロックを分解中だった場合 "slow" にする 226 | if board.contains_q_block then 227 | ripple.slow = true 228 | end 229 | 230 | -- もしフリーズ中だったら ripple も freeze にする 231 | ripple.freeze = board.freeze_timer > 0 232 | end 233 | end 234 | 235 | particles:update_all() 236 | bubbles:update_all() 237 | ions:update_all() 238 | 239 | if not game_over_time then 240 | -- ゲーム中だけ elapsed_time を更新 241 | elapsed_time = t() - start_time 242 | 243 | -- プレーヤーが 2 人であれば、勝ったほうの board に win = true をセット 244 | if #all_players_info == 1 then 245 | if all_players_info[1].board:is_game_over() then 246 | game_over_time = t() 247 | end 248 | else 249 | local board1, board2 = all_players_info[1].board, all_players_info[2].board 250 | 251 | if board1:is_game_over() or board2:is_game_over() then 252 | game_over_time = t() 253 | 254 | if board1.lose then 255 | board2.win = true 256 | end 257 | if board2.lose then 258 | board1.win = true 259 | end 260 | end 261 | end 262 | end 263 | end 264 | 265 | function game_class.render(_ENV) -- override 266 | ripple:render_all() 267 | 268 | for _, each in pairs(all_players_info) do 269 | local board = each.board 270 | 271 | board:render() 272 | 273 | -- カウントダウンの数字はカーソルの上に表示 274 | if board.countdown then 275 | local countdown_sprite_x = { 32, 16, 0 } 276 | sspr( 277 | countdown_sprite_x[board.countdown], 278 | 80, 279 | 16, 280 | 16, 281 | board.offset_x + 16, 282 | 43 283 | ) 284 | end 285 | end 286 | 287 | particles:render_all() 288 | bubbles:render_all() 289 | ions:render_all() 290 | end 291 | 292 | -- ブロックをせりあげる 293 | function game_class._raise(_ENV, player_info, force) 294 | local board = player_info.board 295 | 296 | if not board:is_topped_out() or force then 297 | board.raised_dots = board.raised_dots + 1 298 | 299 | if board.raised_dots == 8 then 300 | board.raised_dots = 0 301 | board:insert_blocks_at_bottom() 302 | board.cursor:move_up(board.rows) 303 | end 304 | end 305 | end 306 | 307 | -- 可能な場合ブロックを自動的にせりあげる 308 | function game_class._auto_raise(_ENV, player_info) 309 | if player_info.board.freeze_timer > 0 or player_info.board:is_busy() then 310 | return 311 | end 312 | 313 | player_info.tick = player_info.tick + 1 314 | 315 | if player_info.tick > auto_raise_frame_count then 316 | _raise(_ENV, player_info, true) 317 | player_info.tick = 0 318 | end 319 | end 320 | 321 | -- ゲーム経過時間を文字列で返す (e.g., "01:23") 322 | function game_class.elapsed_time_string(_ENV) 323 | local length2_number_string_0filled = function(num) 324 | return (num < 10) and "0" .. num or num 325 | end 326 | 327 | return length2_number_string_0filled(flr(elapsed_time / 60)) .. 328 | ":" .. 329 | length2_number_string_0filled(flr(elapsed_time) % 60) 330 | end 331 | -------------------------------------------------------------------------------- /test/block/hover_utest.lua: -------------------------------------------------------------------------------- 1 | require("engine/test/bustedhelper") 2 | require("test/test_helper") 3 | require("lib/board") 4 | 5 | describe('ブロックの hover 状態', function() 6 | local board 7 | 8 | before_each(function() 9 | board = board_class() 10 | end) 11 | 12 | describe('ブロックの下が空の場合', function() 13 | local block 14 | 15 | before_each(function() 16 | block = block_class("h") 17 | end) 18 | 19 | it("ブロックの状態が hover になる", function() 20 | board:put(1, 2, block) 21 | 22 | board:update() 23 | 24 | assert.are.equal("hover", block.state) 25 | end) 26 | 27 | it("hover 状態は 12 フレーム継続する", function() 28 | board:put(1, 2, block) 29 | 30 | -- hover 状態に遷移 31 | board:update() 32 | 33 | for i = 1, 12 do 34 | board:update() 35 | 36 | assert.are.equal("hover", block.state) 37 | end 38 | 39 | -- fall 状態に遷移 40 | board:update() 41 | 42 | assert.are.equal("fall", block.state) 43 | end) 44 | end) 45 | 46 | describe('2 つ積み重なったブロックの下が空の場合', function() 47 | local block1, block2 48 | 49 | before_each(function() 50 | block1 = block_class("h") 51 | block2 = block_class("x") 52 | end) 53 | 54 | it("状態が hover になる", function() 55 | board:put(1, 3, block1) 56 | board:put(1, 2, block2) 57 | 58 | board:update() 59 | 60 | assert.are.equal("hover", block1.state) 61 | assert.are.equal("hover", block2.state) 62 | end) 63 | 64 | it("hover 状態は 12 フレーム継続する", function() 65 | board:put(1, 3, block1) 66 | board:put(1, 2, block2) 67 | 68 | -- hover 状態に遷移 69 | board:update() 70 | 71 | -- hover が 12 フレーム続く 72 | for i = 1, 12 do 73 | board:update() 74 | 75 | assert.are.equal("hover", board:block_at(1, 3).state) 76 | assert.are.equal("hover", board:block_at(1, 2).state) 77 | end 78 | 79 | -- fall 状態に遷移 80 | board:update() 81 | 82 | assert.are.equal("fall", board:block_at(1, 2).state) 83 | assert.are.equal("fall", board:block_at(1, 1).state) 84 | end) 85 | end) 86 | 87 | describe('CNOT の下が空の場合', function() 88 | local control, cnot_x 89 | 90 | before_each(function() 91 | control = control_block(2) 92 | cnot_x = cnot_x_block(1) 93 | end) 94 | 95 | it("状態が hover になる", function() 96 | board:put(1, 2, control) 97 | board:put(2, 2, cnot_x) 98 | 99 | board:update() 100 | 101 | assert.are.equal("hover", control.state) 102 | assert.are.equal("hover", cnot_x.state) 103 | end) 104 | 105 | it("hover 状態は 12 フレーム継続する", function() 106 | board:put(1, 2, control) 107 | board:put(2, 2, cnot_x) 108 | 109 | -- hover 状態に遷移 110 | board:update() 111 | 112 | for i = 1, 12 do 113 | board:update() 114 | 115 | assert.are.equal("hover", control.state) 116 | assert.are.equal("hover", cnot_x.state) 117 | end 118 | 119 | -- fall 状態に遷移 120 | board:update() 121 | 122 | assert.are.equal("fall", control.state) 123 | assert.are.equal("fall", cnot_x.state) 124 | end) 125 | end) 126 | 127 | describe('2 つ積み重なった CNOT の下が空の場合', function() 128 | local control1, cnot_x1, control2, cnot_x2 129 | 130 | before_each(function() 131 | control1 = control_block(2) 132 | cnot_x1 = cnot_x_block(1) 133 | control2 = control_block(1) 134 | cnot_x2 = cnot_x_block(2) 135 | end) 136 | 137 | it("両方のおじゃまブロックとも状態が hover になる", function() 138 | board:put(1, 3, control1) 139 | board:put(2, 3, cnot_x1) 140 | board:put(1, 2, cnot_x2) 141 | board:put(2, 2, control2) 142 | 143 | board:update() 144 | 145 | assert.are.equal("hover", control1.state) 146 | assert.are.equal("hover", cnot_x1.state) 147 | assert.are.equal("hover", control2.state) 148 | assert.are.equal("hover", cnot_x2.state) 149 | end) 150 | 151 | it("hover 状態は 12 フレーム継続する", function() 152 | board:put(1, 3, control1) 153 | board:put(2, 3, cnot_x1) 154 | board:put(1, 2, cnot_x2) 155 | board:put(2, 2, control2) 156 | 157 | -- hover 状態に遷移 158 | board:update() 159 | 160 | for i = 1, 12 do 161 | board:update() 162 | 163 | assert.are.equal("hover", control1.state) 164 | assert.are.equal("hover", cnot_x1.state) 165 | assert.are.equal("hover", control2.state) 166 | assert.are.equal("hover", cnot_x2.state) 167 | end 168 | 169 | -- fall 状態に遷移 170 | board:update() 171 | 172 | assert.are.equal("fall", control1.state) 173 | assert.are.equal("fall", cnot_x1.state) 174 | assert.are.equal("fall", control2.state) 175 | assert.are.equal("fall", cnot_x2.state) 176 | end) 177 | end) 178 | 179 | describe('SWAP の下が空の場合', function() 180 | local swap_left, swap_right 181 | 182 | before_each(function() 183 | swap_left = swap_block(2) 184 | swap_right = swap_block(1) 185 | end) 186 | 187 | it("状態が hover になる", function() 188 | board:put(1, 2, swap_left) 189 | board:put(2, 2, swap_right) 190 | 191 | board:update() 192 | 193 | assert.are.equal("hover", swap_left.state) 194 | assert.are.equal("hover", swap_right.state) 195 | end) 196 | 197 | it("hover 状態は 12 フレーム継続する", function() 198 | board:put(1, 2, swap_left) 199 | board:put(2, 2, swap_right) 200 | 201 | -- hover 状態に遷移 202 | board:update() 203 | 204 | for i = 1, 12 do 205 | board:update() 206 | 207 | assert.are.equal("hover", swap_left.state) 208 | assert.are.equal("hover", swap_right.state) 209 | end 210 | 211 | -- fall 状態に遷移 212 | board:update() 213 | 214 | assert.are.equal("fall", swap_left.state) 215 | assert.are.equal("fall", swap_right.state) 216 | end) 217 | end) 218 | 219 | describe('2 つ積み重なった SWAP の下が空の場合', function() 220 | local swap_left1, swap_right1, swap_left2, swap_right2 221 | 222 | before_each(function() 223 | swap_left1 = swap_block(2) 224 | swap_right1 = swap_block(1) 225 | swap_left2 = swap_block(3) 226 | swap_right2 = swap_block(1) 227 | end) 228 | 229 | it("両方のおじゃまブロックとも状態が hover になる", function() 230 | board:put(1, 3, swap_left1) 231 | board:put(2, 3, swap_right1) 232 | board:put(1, 2, swap_left2) 233 | board:put(3, 2, swap_right2) 234 | 235 | board:update() 236 | 237 | assert.are.equal("hover", swap_left1.state) 238 | assert.are.equal("hover", swap_right1.state) 239 | assert.are.equal("hover", swap_left2.state) 240 | assert.are.equal("hover", swap_right2.state) 241 | end) 242 | 243 | it("hover 状態は 12 フレーム継続する", function() 244 | board:put(1, 3, swap_left1) 245 | board:put(2, 3, swap_right1) 246 | board:put(1, 2, swap_left2) 247 | board:put(3, 2, swap_right2) 248 | 249 | -- hover 状態に遷移 250 | board:update() 251 | 252 | for i = 1, 12 do 253 | board:update() 254 | 255 | assert.are.equal("hover", swap_left1.state) 256 | assert.are.equal("hover", swap_right1.state) 257 | assert.are.equal("hover", swap_left2.state) 258 | assert.are.equal("hover", swap_right2.state) 259 | end 260 | 261 | -- fall 状態に遷移 262 | board:update() 263 | 264 | assert.are.equal("fall", swap_left1.state) 265 | assert.are.equal("fall", swap_right1.state) 266 | assert.are.equal("fall", swap_left2.state) 267 | assert.are.equal("fall", swap_right2.state) 268 | end) 269 | end) 270 | 271 | describe('おじゃまブロックの下が空の場合', function() 272 | local garbage 273 | 274 | before_each(function() 275 | garbage = garbage_block() 276 | end) 277 | 278 | it("状態が fall になる", function() 279 | board:put(1, 2, garbage) 280 | 281 | board:update() 282 | 283 | assert.are.equal("hover", garbage.state) 284 | end) 285 | 286 | it("hover 状態は 12 フレーム継続する", function() 287 | board:put(1, 2, garbage) 288 | 289 | -- hover 状態に遷移 290 | board:update() 291 | 292 | for i = 1, 12 do 293 | board:update() 294 | 295 | assert.are.equal("hover", garbage.state) 296 | end 297 | 298 | -- fall 状態に遷移 299 | board:update() 300 | 301 | assert.are.equal("fall", garbage.state) 302 | end) 303 | end) 304 | 305 | describe('2 つ積み重なったおじゃまブロックの下が空の場合', function() 306 | local garbage1, garbage2 307 | 308 | before_each(function() 309 | garbage1 = garbage_block() 310 | garbage2 = garbage_block() 311 | end) 312 | 313 | it("両方のおじゃまブロックとも状態が hover になる", function() 314 | board:put(1, 3, garbage1) 315 | board:put(1, 2, garbage2) 316 | 317 | board:update() 318 | 319 | assert.are.equal("hover", garbage1.state) 320 | assert.are.equal("hover", garbage2.state) 321 | end) 322 | 323 | it("hover 状態は 12 フレーム継続する", function() 324 | board:put(1, 3, garbage1) 325 | board:put(1, 2, garbage2) 326 | 327 | -- hover 状態に遷移 328 | board:update() 329 | 330 | for i = 1, 12 do 331 | board:update() 332 | 333 | assert.are.equal("hover", garbage1.state) 334 | assert.are.equal("hover", garbage2.state) 335 | end 336 | 337 | -- fall 状態に遷移 338 | board:update() 339 | 340 | assert.are.equal("fall", garbage1.state) 341 | assert.are.equal("fall", garbage2.state) 342 | end) 343 | end) 344 | 345 | describe('ホバー中のブロックにおじゃまブロックが落ちてきた場合', function() 346 | local hover_block 347 | local garbage 348 | 349 | before_each(function() 350 | hover_block = block_class("h") 351 | garbage = garbage_block(3) 352 | end) 353 | 354 | it("ホバー中のブロックのタイマーをおじゃまブロックに伝搬", function() 355 | board:put(1, 4, garbage) 356 | board:put(2, 2, hover_block) 357 | hover_block:hover() 358 | garbage:fall() 359 | 360 | -- ホバーの残りフレームを 12 - 2 = 10 にする 361 | board:update() 362 | board:update() 363 | 364 | assert.are.equal(10, hover_block.timer) 365 | assert.are.equal(10, garbage.timer) 366 | end) 367 | end) 368 | end) 369 | --------------------------------------------------------------------------------