pico-os-app 1 395 breakout 2.0.0 ブロック崩し ブロック崩し 27133 50029ed1e5484e13fb9db58990328d2b6b137e723b7081341fdbe120c6054044 - pico.gameのタイルマップとスプライトで作る全20ステージのブロック崩し cdae7ba29d76143cb10ee4c5bf4c9c78c3a55dfd5d6cd0df25ff0cbacad30c5d kimu1109 - ed25519 cdae7ba29d76143cb10ee4c5bf4c9c78c3a55dfd5d6cd0df25ff0cbacad30c5d fd0e4dedd5b4513adccbea6480553fdd160faac60ce67a805cd47f7eb7c9240551ae8dee8f0db68812490a3fab9bc37020dcc4b148e653fc26352df31e908f03 app.cfg 173 4fcbe021403009e0d7ec4772038ee54b9cab74cb9d6803707cd155b0ba291e54 icon.pimg 543 f25c4e12d7ce1a8c418b67dbadf5854f74148d513ddec45055940739ca445fed lib.lua 10654 6841924351b1bfc8de355fb0128d9330aa758b5533c5df25e3de1c464450fdcf main.lua 15420 dadfe72ab8dd09e29412e26b5480b20d4a3aba6f2165f0e380e318bfa2c425fe tiles.pimg 343 29a103a6e61c568f2d49c8bfe9c047a842f3c85c6c88219a3c56357485138bb4 name=ブロック崩し id=breakout description=pico.gameのタイルマップとスプライトで作る全20ステージのブロック崩し icon=icon.pimg version=2.0.0 00Ý( $!        ’     À-- ブロック崩しのステージ定義と自動プレイの狙い。ステージの生成ロジックが大きいので -- 本体(main.lua)と分けてある。LuaSceneが本体(main.lua)より -- 先にこのファイルを読み込み・実行し(同じディレクトリの"lib.lua"を自動で探す)、 -- 結果はグローバル変数STAGESとして本体から見える(LuaScene.hppのクラスコメント参照)。 -- 以前はmain.lua自身がpico.sd_read()+Luaのload()+pcall()で実行時にこのファイルを -- コンパイルしていたが、実機でコア0のスタックオーバーフローを起こしコア1(音声専用)の -- スタックを巻き込む事故につながったため、この形へ改めた。 -- -- 「形(shape: どのマスにブロックを置くか)」「速度tier(tier: 何色/何速で置くか)」 -- 「壁(wall: そのうちどれを壊せなくするか)」の3つを独立した関数として組み合わせる -- 設計にしてある。以前はshape/tier/wallが1つの関数に混ざっており、壁を「壊せる -- ブロックへ一定確率で上書きする」形で足していたため、運が悪いと壊せるブロックの -- 四方をすべて壁で囲んでしまい、そのブロックに二度と当たれず詰む(クリア不能になる) -- 事故が起きた。3つに分けたことで壁の形を意図的にデザインできるようになり、 -- 加えて末尾のensureSolvable()が「壊せるブロックがパドル側の開放空間へ本当に -- 届くか」を毎ステージ機械的に検証するので、デザインを間違えても詰みステージは -- 出荷されない(万一届かないブロックがあれば、そのマスだけ自動的に空へ戻す)。 local COLS = 8 local WALL = -1 -- main.lua側のWALL_TIERと値を一致させること -- 速度tierのパターン。1が最速(赤)、6が最遅(青)。 -- tierTop: 上段ほど速い(元祖ブロック崩しの定番配置。序盤はこれで様子を掴んでもらう) -- tierBottom: 下段(パドルに近い)ほど速い。当てた瞬間ボールが速くなるので、 -- 反応時間が一番短いタイミングで一番速くなる=同じ壁配置でも体感難易度が上がる。 -- 後半ステージほどこちらを多く使う。 -- tierEdges: 上下の端が速く、中段が遅い。中央に固まりがちな詰まった形状に -- メリハリを付けるための3つ目のバリエーション。 local function tierTop(r, rows) return ((r - 1) % 6) + 1 end local function tierBottom(r, rows) return ((rows - r) % 6) + 1 end local function tierEdges(r, rows) local mid = (rows + 1) / 2 local maxd = math.floor((rows - 1) / 2 + 0.5) local d = math.floor(math.abs(r - mid) + 0.5) return ((maxd - d) % 6) + 1 end -- 形(shape)。(r, c, rows) -> そのマスにブロックを置くかどうか local function shapeFull(r, c, rows) return true end local function shapeChecker(odd) local m = odd and 1 or 0 return function(r, c, rows) return (r + c) % 2 == m end end local function shapeBorder(r, c, rows) return r == 1 or r == rows or c == 1 or c == COLS end local function shapeStripes(gap) return function(r, c, rows) return c % gap ~= 0 end end local function shapePyramid(r, c, rows) local half = (COLS - r) / 2 return c > half and c <= COLS - half end local function shapeInvPyramid(r, c, rows) local half = (r - 1) / 2 return c > half and c <= COLS - half end local function shapeColumns2(r, c, rows) return c <= 3 or c >= COLS - 2 end local function shapeCross(r, c, rows) local midr, midc = math.ceil(rows / 2), math.ceil(COLS / 2) return c == midc or (r >= midr - 1 and r <= midr + 1) end local function shapeDiamond(r, c, rows) local cy, cx = (rows + 1) / 2, (COLS + 1) / 2 local rad = math.min(cx, cy) local d = math.abs(r - cy) + math.abs(c - cx) * (cy / cx) return d <= rad end local function shapeRings(r, c, rows) local cy, cx = (rows + 1) / 2, (COLS + 1) / 2 local d = math.floor(math.max(math.abs(r - cy), math.abs(c - cx))) return d % 2 == 0 end -- 一定間隔で1マスだけ穴を開ける「窓」。以前のrandomHoles(乱数で穴あけ)の -- 置き換え。乱数を使わないので見た目が整い、壁の安全性の議論からも独立する -- (これは「壁」ではなく単なる空きマスなので、そもそも詰みの原因にならない) local function shapeWindows(holeEvery) return function(r, c, rows) return not (r % holeEvery == 0 and c % holeEvery == 0) end end -- 壁(wall)。(r, c, rows) -> そのマス(shapeが立っている前提)を壊せなくするか -- -- wallPillars: 指定した列をまるごと壁にする「柱」。列は常に一番下の行まで -- 壁で埋まるが、挟まれた列(柱にしていない列)は自分の列の中で下へ辿れば -- 必ずパドル側の開放空間へ抜けられるので、他の列を壁にしても詰まない。 local function wallPillars(cols) local set = {} for _, c in ipairs(cols) do set[c] = true end return function(r, c, rows) return set[c] == true end end -- wallFrameGate: 外枠(上端・左端・右端)を壁にし、下端だけは開けておく「門」。 -- 内側(枠の内部)は壁にしないので、下端の開口部から入って上へ辿れば -- 必ず内側の壊せるブロックへ届く。 local function wallFrameGate() return function(r, c, rows) return (r == 1 or c == 1 or c == COLS) and r ~= rows end end -- wallDiagonal: 斜めの帯を壁にする。period(周期)を3以上にしてあるのは、 -- 隣接マスを壁で埋め尽くさないため(壊せるブロックの上下左右のどこかは -- 必ず空くように間隔を空けてある)。 local function wallDiagonal(period, phase) return function(r, c, rows) return (r + c + phase) % period == 0 end end -- 詰み防止の安全網。パドル側に開放されている最下段(rows行目)を起点に、 -- 壁(WALL)だけを障害物として4方向へ塗りつぶす(壊せるブロックはいずれ -- 壊されて空になるので、ここでは障害物として扱わない)。塗りつぶしが -- 届かなかった壊せるブロックが残っていたら、そのマスを空(0)へ戻して -- ステージから除外する——「絶対にクリア不能なブロックを残さない」ことを -- 幾何学デザインの正しさに頼らず機械的に保証するための最終防衛ライン。 local function ensureSolvable(rows, grid) local visited = {} for r = 1, rows do visited[r] = {} end local queue, qh, qt = {}, 1, 0 local function push(r, c) if r < 1 or r > rows or c < 1 or c > COLS then return end if visited[r][c] or grid[r][c] == WALL then return end visited[r][c] = true qt = qt + 1 queue[qt] = { r, c } end for c = 1, COLS do push(rows, c) end while qh <= qt do local r, c = queue[qh][1], queue[qh][2] qh = qh + 1 push(r - 1, c); push(r + 1, c); push(r, c - 1); push(r, c + 1) end for r = 1, rows do for c = 1, COLS do if grid[r][c] > 0 and not visited[r][c] then grid[r][c] = 0 end end end end -- shape/tier/wallの3つを合成して実際のマス目(grid)を確定させ、 -- ensureSolvable()を必ず通してから返す local function stage(rows, shapeFn, tierFn, wallFn) tierFn = tierFn or tierTop local grid = {} for r = 1, rows do grid[r] = {} for c = 1, COLS do if shapeFn(r, c, rows) then grid[r][c] = (wallFn and wallFn(r, c, rows)) and WALL or tierFn(r, rows) else grid[r][c] = 0 end end end ensureSolvable(rows, grid) return { rows = rows, cell = function(r, c) return grid[r][c] end } end -- 自動プレイ(バッテリー消費計測等で放置プレイさせるためのもの)の狙い。狙うのは -- 「パドルへ向かって落ちてくるボールのうち一番下にいるもの」で、無ければ(全ボールが -- 上向きの間)先頭のボールを追う。完璧な反射は狙わずパドル中央で追尾するだけの単純な -- 実装(ミスもする=ライフが減りゲームオーバーへ至ることも許容)。 -- ボールは { cx, cy(中心), dx, dy(速度) } を持つスプライト。戻り値は狙うx(無ければfallback) local function autoTargetX(balls, fallback) local best, bestY = nil, -1 for i = 1, #balls do local b = balls[i] if b.dy > 0 and b.cy > bestY then best, bestY = b, b.cy end end if not best and #balls > 0 then best = balls[1] end return best and best.cx or fallback end -- 序盤(1〜5)は壁なし・上段が速いだけのチュートリアル。6以降は -- 柱/門/斜め帯の壁を幾何学的に配置しつつ、tierBottom(下段が速い)の -- 採用比率を上げていくことで、壁の物量に頼らず難易度を積み増す。 -- LuaSceneが本体より先にこのファイルを実行するだけなので、returnではなく -- グローバル変数への代入で結果を渡す(本体側は`local STAGES = STAGES`で受け取る) STAGES = { autoTargetX = autoTargetX, cols = COLS, list = { stage(3, shapeFull, tierTop), stage(4, shapeChecker(false), tierTop), stage(4, shapeBorder, tierTop), stage(4, shapeStripes(3), tierTop), stage(4, shapeFull, tierTop), stage(5, shapePyramid, tierTop, wallPillars({ 4 })), stage(5, shapeInvPyramid, tierBottom, wallPillars({ 5 })), stage(5, shapeColumns2, tierTop, wallPillars({ 1, 8 })), stage(5, shapeChecker(true), tierEdges, wallDiagonal(5, 0)), stage(5, shapeCross, tierTop, wallPillars({ 4 })), stage(6, shapeBorder, tierBottom, wallPillars({ 1, 8 })), stage(6, shapeStripes(2), tierTop, wallDiagonal(6, 3)), stage(6, shapeDiamond, tierBottom, wallPillars({ 4, 5 })), stage(6, shapeWindows(3), tierEdges, wallDiagonal(4, 1)), stage(7, shapePyramid, tierBottom, wallPillars({ 3, 6 })), stage(7, shapeRings, tierTop, wallDiagonal(3, 0)), stage(7, shapeChecker(false), tierBottom, wallPillars({ 2, 4, 6 })), stage(7, shapeWindows(2), tierBottom, wallDiagonal(3, 2)), stage(8, shapeDiamond, tierBottom, wallPillars({ 2, 4, 5, 7 })), stage(8, shapeFull, tierBottom, wallFrameGate()), }, } -- ブロック崩し。pico.game(2Dゲームの簡易エンジン)で作っている。 -- ブロック=タイルマップ(tiles.pimg。1〜6が速さの段、7が壊せない灰)。ボール・パドル・アイテム=スプライト。 -- ステージクリア等は g:state の枠。ステージの定義は lib.lua(グローバルSTAGES)。 -- コントローラー: 左右=パドル、A/START/上=発射・次へ、HOME=戻る。 -- ※LuaSceneが読むのは16KiBまで(日本語コメントは1文字3バイト)。足すときは余裕を見ること local game = require("pico.game") local STAGES = STAGES local function clamp(v, lo, hi) if v < lo then return lo end if v > hi then return hi end return v end local COLS = STAGES.cols -- 速さの段。1が最速(赤)、6が最遅(青) local TIER_MULT = { 2.0, 1.6, 1.35, 1.25, 1.0, 0.7 } local TIER_POINTS = { 60, 50, 40, 30, 20, 10 } local WALL_TIER = -1 -- lib.luaのWALLと同じ値 local T_WALL = 7 local function baseSpeed(stage) return 110 + stage * 5 end -- px/秒 local BLOCK_W, BLOCK_H, GAP = 27, 12, 2 local TW, TH = BLOCK_W + GAP, BLOCK_H + GAP -- タイル(ブロック+隙間) local BALL_R = 4 local PADDLE_H = 8 -- パドル幅はステージが進むほど狭くする(5ステージごとに4px、最小28px) local PADDLE_W_MAX, PADDLE_W_MIN, PADDLE_W_STEP, PADDLE_W_STAGE_SPAN = 40, 28, 4, 5 local function paddleWidthForStage(stage) local w = PADDLE_W_MAX - math.floor((stage - 1) / PADDLE_W_STAGE_SPAN) * PADDLE_W_STEP return math.max(PADDLE_W_MIN, w) end -- drawは w x h の外へ描かない(残る)。fill_circleは直径2r+1画素なので、ボールは9x9・アイテムは11x11 local BALL_SIZE = BALL_R * 2 + 1 local ITEM_SIZE = 11 local ITEM_TRIBALL, ITEM_DOUBLE, ITEM_SLOW = 1, 2, 3 local ITEM_FALL_SPEED = 70 -- px/秒 local DROP_CHANCE = 0.2 local MAX_BALLS = 16 local MAX_ITEMS = 4 local STEP_MAX = 6 -- サブステップ分割の閾値(px)。低フレームレートでの貫通防止 local PAD_SPEED = 220 -- コントローラーでのパドルの速さ(px/秒) local MAX_ANGLE = 1.1 -- パドル反射の最大角(ラジアン) local g = game.new{ bg = 15 } local cw, ch = g.vw, g.vh local MARGIN = 4 local field_x = MARGIN local field_w = cw - MARGIN * 2 local TOP_WALL = 22 local BLOCK_TOP = TOP_WALL + 2 local BLOCK_X = field_x + (field_w - (COLS * TW - GAP)) // 2 local paddle_y = ch - 24 local BOTTOM_LIMIT = ch local img = g:image(pico.path_join(pico.app_dir(), "tiles.pimg")) local map, paddle local paddle_w = paddleWidthForStage(1) local score, lives, stage_idx = 0, 3, 1 local blocks_remaining = 0 local balls = {} local paddle_cx = field_x + field_w / 2 local auto = false -- 自動プレイ(右上の「自動」。バッテリー計測用の放置プレイ) local auto_btn -- 効果音はch2で1フレーム1回、優先度の高いもの(ボールが多いと命令の列が溢れる) local snd local function se(pri, f, ms, wave) if not snd or pri > snd[1] then snd = { pri, f, ms, wave or "pulse25" } end end -- ジングルは短いMMLを曲として鳴らす(ch1) local function jingle(mml) pico.music_play_text("#tempo 200\nA @pulse25 v11 q7 o5 l16 " .. mml) end local function hudDirty() g:dirty(0, 0, g.vw, TOP_WALL) end -- ---- スプライトの絵 ---- local function ballDraw(_, x, y) pico.fill_circle(x + BALL_R, y + BALL_R, BALL_R, 0) end -- アイテム: 1=3つに増える(黄の四角) 2=倍にする(シアンの円) 3=ゆっくり(青の三角) local ITEM_DRAWS = { function(_, x, y) pico.fill_rect(x, y, ITEM_SIZE, ITEM_SIZE, 14) end, function(_, x, y) pico.fill_circle(x + ITEM_SIZE // 2, y + ITEM_SIZE // 2, ITEM_SIZE // 2, 11) end, function(_, x, y) pico.fill_triangle(x, y + ITEM_SIZE - 1, x + ITEM_SIZE // 2, y, x + ITEM_SIZE - 1, y + ITEM_SIZE - 1, 9) end, } local function newBall(cx, cy, dx, dy) local b = g:sprite{ x = cx - BALL_R, y = cy - BALL_R, w = BALL_SIZE, h = BALL_SIZE, layer = 3, draw = ballDraw, tag = "ball", cx = cx, cy = cy, dx = dx, dy = dy } balls[#balls + 1] = b return b end local function placeBall(b) b.x, b.y = b.cx - BALL_R, b.cy - BALL_R end -- ---- ステージ ---- local function loadStage(idx) local data = STAGES.list[idx] g:clear() -- 前のステージのものを全部消す local bytes, parts = {}, {} blocks_remaining = 0 for r = 1, data.rows do for c = 1, COLS do local tier = data.cell(r, c) local v = 0 if tier == WALL_TIER then v = T_WALL elseif tier ~= 0 then v = tier; blocks_remaining = blocks_remaining + 1 end bytes[c] = v end parts[r] = string.char(table.unpack(bytes, 1, COLS)) end map = g:tilemap{ image = img, tile_w = TW, tile_h = TH, x = BLOCK_X, y = BLOCK_TOP, data = table.concat(parts), cols = COLS } paddle_w = paddleWidthForStage(idx) paddle = g:sprite{ x = 0, y = paddle_y, w = paddle_w, h = PADDLE_H, color = 1, tag = "paddle", layer = 2 } balls = {} hudDirty() end local function setPaddle(x) paddle_cx = clamp(x, field_x + paddle_w / 2, field_x + field_w - paddle_w / 2) paddle.x = math.floor(paddle_cx - paddle_w / 2) end local function clearItems() for _, it in ipairs(g:find("item")) do it:remove() end end local function readyBall() for _, b in ipairs(balls) do b:remove() end balls = {} clearItems() setPaddle(field_x + field_w / 2) newBall(paddle_cx, paddle_y - BALL_R * 2 - 1, 0, 0) g:go("ready") end local function startGame() score, lives, stage_idx = 0, 3, 1 loadStage(stage_idx) readyBall() end local function applyTierSpeed(b, tier) local speed = baseSpeed(stage_idx) * TIER_MULT[tier] local mag = math.sqrt(b.dx * b.dx + b.dy * b.dy) if mag > 0 then local k = speed / mag b.dx, b.dy = b.dx * k, b.dy * k end end -- ---- アイテム ---- local function spawnItem(x, y) if #g:find("item") >= MAX_ITEMS then return end local kind = math.random(1, 3) g:sprite{ x = x - ITEM_SIZE / 2, y = y - ITEM_SIZE / 2, w = ITEM_SIZE, h = ITEM_SIZE, vy = ITEM_FALL_SPEED, layer = 1, tag = "item", kind = kind, draw = ITEM_DRAWS[kind], on_update = function(s) if s.y > BOTTOM_LIMIT then s:remove() end end } end local function spawnExtraBalls(n) local speed = baseSpeed(stage_idx) for i = 1, n do if #balls < MAX_BALLS then local ang = (i - (n + 1) / 2) * 0.35 newBall(paddle_cx, paddle_y - BALL_R * 2 - 1, speed * math.sin(ang), -speed * math.cos(ang)) end end end local function doubleBalls() local n = #balls for i = 1, n do if #balls < MAX_BALLS then local b = balls[i] newBall(b.cx, b.cy, -b.dx, b.dy) end end end local function slowAllBalls() for i = 1, #balls do applyTierSpeed(balls[i], #TIER_MULT) end end g:collide("paddle", "item", function(_, it) it:remove() se(4, 1760, 120, "pulse50") if it.kind == ITEM_TRIBALL then spawnExtraBalls(3) elseif it.kind == ITEM_DOUBLE then doubleBalls() else slowAllBalls() end end) -- ---- ボールの動き ---- local function destroyBlock(c, r, tier) map:set(c, r, 0) blocks_remaining = blocks_remaining - 1 se(3, 1500 - tier * 150, 50, "pulse50") score = score + TIER_POINTS[tier] hudDirty() if math.random() < DROP_CHANCE then spawnItem(BLOCK_X + c * TW + BLOCK_W / 2, BLOCK_TOP + r * TH + BLOCK_H / 2) end end -- ボールの外接矩形がかかるマスだけ調べ、1つ見つけたら反射して終わり local function collideBlocks(b) local c0 = math.floor((b.cx - BALL_R - BLOCK_X) / TW) local c1 = math.floor((b.cx + BALL_R - BLOCK_X) / TW) local r0 = math.floor((b.cy - BALL_R - BLOCK_TOP) / TH) local r1 = math.floor((b.cy + BALL_R - BLOCK_TOP) / TH) for r = r0, r1 do for c = c0, c1 do local v = map:get(c, r) if v ~= 0 then local bx, by = BLOCK_X + c * TW, BLOCK_TOP + r * TH local nx = clamp(b.cx, bx, bx + BLOCK_W) local ny = clamp(b.cy, by, by + BLOCK_H) local dx, dy = b.cx - nx, b.cy - ny if dx * dx + dy * dy < BALL_R * BALL_R then local left = (b.cx + BALL_R) - bx local right = (bx + BLOCK_W) - (b.cx - BALL_R) local top = (b.cy + BALL_R) - by local bottom = (by + BLOCK_H) - (b.cy - BALL_R) local m = math.min(left, right, top, bottom) if m == left then b.cx = bx - BALL_R; b.dx = -math.abs(b.dx) elseif m == right then b.cx = bx + BLOCK_W + BALL_R; b.dx = math.abs(b.dx) elseif m == top then b.cy = by - BALL_R; b.dy = -math.abs(b.dy) else b.cy = by + BLOCK_H + BALL_R; b.dy = math.abs(b.dy) end if v == T_WALL then se(2, 180, 40, "triangle") -- 壊せないブロック: 反射だけ else applyTierSpeed(b, v) destroyBlock(c, r, v) end return end end end end end local function collidePaddle(b) local px = paddle_cx - paddle_w / 2 if b.cx + BALL_R >= px and b.cx - BALL_R <= px + paddle_w and b.cy + BALL_R >= paddle_y and b.cy - BALL_R <= paddle_y + PADDLE_H then local offset = clamp((b.cx - paddle_cx) / (paddle_w / 2), -1, 1) local speed = math.sqrt(b.dx * b.dx + b.dy * b.dy) local angle = offset * MAX_ANGLE b.dx = speed * math.sin(angle) b.dy = -speed * math.cos(angle) b.cy = paddle_y - BALL_R se(2, 520, 40) end end local function stepBall(b, dtSec) local dist = math.sqrt(b.dx * b.dx + b.dy * b.dy) * dtSec local steps = math.max(1, math.ceil(dist / STEP_MAX)) local subDt = dtSec / steps for _ = 1, steps do b.cx = b.cx + b.dx * subDt b.cy = b.cy + b.dy * subDt local dx, dy = b.dx, b.dy if b.cx - BALL_R < field_x then b.cx = field_x + BALL_R; b.dx = math.abs(b.dx) elseif b.cx + BALL_R > field_x + field_w then b.cx = field_x + field_w - BALL_R; b.dx = -math.abs(b.dx) end if b.cy - BALL_R < TOP_WALL then b.cy = TOP_WALL + BALL_R; b.dy = math.abs(b.dy) end if dx ~= b.dx or dy ~= b.dy then se(1, 330, 20, "pulse12") end collideBlocks(b) if b.dy > 0 then collidePaddle(b) end if b.cy - BALL_R > BOTTOM_LIMIT then b.lost = true return end end end -- ---- 流れ ---- local function stageClear() clearItems() if stage_idx >= #STAGES.list then jingle("c e g > c e g > c4") g:go("win") else jingle("c e g > c4") g:go("clear") end end local function loseLife() lives = lives - 1 hudDirty() if lives <= 0 then jingle("l8 e d c < g2") clearItems() g:go("over") else jingle("l8 g e c4") readyBall() end end local function launch() local speed = baseSpeed(stage_idx) local b = balls[1] b.dx, b.dy = 0, -speed g:go("play") se(2, 880, 60) end -- パドル: 指(触れている間そこへ)・コントローラー・自動プレイ local function control(dt, playing) local t = g.touch local dir = (g:down("right") and 1 or 0) - (g:down("left") and 1 or 0) if auto then if playing then setPaddle(STAGES.autoTargetX(balls, paddle_cx) - 20) end -- あえてずらす else if t.down then setPaddle(t.x) end if dir ~= 0 then setPaddle(paddle_cx + dir * PAD_SPEED * dt) end end end -- 発射・次へ・もう一度(タップ、A、START、上。自動プレイ中は常に) local function goKey() return auto or g.touch.pressed or g:pressed("a") or g:pressed("start") or g:pressed("up") end -- ---- 画面 ---- local function overlay(ox, oy, l1, l2, l3) local x, y, w, h = ox + 20, oy + 96, cw - 40, l3 and 84 or 60 pico.fill_rect(x, y, w, h, 0) pico.draw_rect(x, y, w, h, 15) local lines = { l1, l2, l3 } for i = 1, l3 and 3 or 2 do local t = lines[i] pico.draw_text(x + (w - pico.text_width(t, 0)) // 2, y + 8 + (i - 1) * 24, t, i == 1 and 14 or 15, 0) end end function g:on_update() if self:pressed("back") then pico.pop() return end if self:pressed("auto") then auto = not auto self:dirty(auto_btn.x, auto_btn.y, auto_btn.w, auto_btn.h) end for _, b in ipairs(balls) do placeBall(b) end if snd then pico.sound_play(2, snd[2], snd[3], { wave = snd[4], volume = 9, envelope = -3 }) snd = nil end end function g:on_draw_world(ox, oy) pico.draw_rect(ox + field_x, oy + TOP_WALL, field_w, BOTTOM_LIMIT - TOP_WALL, 7) end function g:on_draw(ox, oy) pico.draw_text(ox + 46, oy + 3, "St" .. stage_idx .. "/" .. #STAGES.list .. " Sc" .. score .. " L" .. lives, 0, 0) end g:button{ name = "back", x = 2, y = 0, w = 40, h = 22, label = "left" } auto_btn = g:button{ name = "auto", x = cw - 43, y = 0, w = 41, h = 22, draw = function(b, x, y, w, h) pico.fill_rect(x + 1, y + 1, w - 2, h - 2, auto and 2 or 8) pico.draw_rect(x + 1, y + 1, w - 2, h - 2, 0) pico.draw_text(x + (w - pico.text_width("自動", 0)) // 2, y + 3, "自動", 15, 0) end } g:state("ready", { update = function(_, dt) control(dt, false) local b = balls[1] b.cx, b.cy = paddle_cx, paddle_y - BALL_R * 2 - 1 if goKey() then launch() end end, draw = function(_, ox, oy) local t = "タップで発射" pico.draw_text(ox + (cw - pico.text_width(t, 0)) // 2, oy + paddle_y - 36, t, 8, 0) end, }) g:state("play", { update = function(_, dt) control(dt, true) for i = #balls, 1, -1 do stepBall(balls[i], dt) if balls[i].lost then balls[i]:remove() table.remove(balls, i) end end if #balls == 0 then loseLife() elseif blocks_remaining == 0 then stageClear() end end, }) g:state("clear", { update = function() if goKey() then stage_idx = stage_idx + 1 loadStage(stage_idx) readyBall() end end, draw = function(_, ox, oy) overlay(ox, oy, "ステージ" .. stage_idx .. "クリア!", "タップで次へ") end, }) g:state("over", { update = function() if goKey() then startGame() end end, draw = function(_, ox, oy) overlay(ox, oy, "ゲームオーバー", "スコア:" .. score, "タップでもう一度") end, }) g:state("win", { update = function() if goKey() then startGame() end end, draw = function(_, ox, oy) overlay(ox, oy, "全ステージクリア!", "スコア:" .. score, "タップで最初から") end, }) pico.on_back(function() pico.pop() end) math.randomseed(pico.get_time().sec * 1000 + pico.get_time().min) startGame() Ë                                                            ÿ™