diff --git a/README.md b/README.md index de9bb21..a290774 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,9 @@ TETR.IO 스타일의 **모바일 실시간 대전 블록 스태커** 웹 게임. 이 문서는 앱 제작을 위한 **최종 빌드 진입점(entry point)** 이다. 모든 설계 결정·수치·에셋의 **정본(source of truth)은 [`TETRIG-최종명세서.md`](./TETRIG-최종명세서.md)** 이며, 구현 시 반드시 명세서와 UX 목업을 함께 열어 작업한다. -> **클라이언트 변경 (2026-08-22 사용자 결정)**: 플레이스토어 출시를 위해 클라이언트를 **네이티브 Android (Kotlin + Jetpack Compose)** 로 제작한다. 소스는 [`android/`](./android). 코어 게임 로직은 UI와 분리된 순수 Kotlin 모듈 [`android/engine`](./android/engine) 에 있고 JUnit 으로 검증한다(SRS 킥테이블·7-bag·T-spin 판정·점수·좌표 등 §4·§13 데이터는 그대로 이식됨). 서버 계획(Cloudflare Workers + D1 + WebSocket)은 변경 없이 유지된다. 초기 웹(HTML) 프로토타입(`src/`, `index.html`)은 검증된 **동작 레퍼런스**로 남겨둔다. +> **클라이언트 변경 (2026-08-22 사용자 결정)**: 플레이스토어 + 앱스토어(아이폰) 동시 출시를 위해 클라이언트를 **크로스플랫폼 Flutter (Dart, Android + iOS)** 로 제작한다. 소스는 [`app/`](./app). 코어 게임 로직은 UI와 분리된 순수 Dart 모듈 [`app/lib/engine.dart`](./app/lib/engine.dart) 에 있고 `flutter test` 로 검증한다(SRS 킥테이블·7-bag·T-spin 판정·점수·좌표 등 §4·§13 데이터는 그대로 이식됨). 서버 계획(Cloudflare Workers + D1 + WebSocket)은 변경 없이 유지된다. 초기 웹(HTML) 프로토타입(`src/`, `index.html`)은 검증된 **동작 레퍼런스**로 남겨둔다. +> +> **iOS 빌드 제약**: Android(APK/AAB)는 리눅스에서 빌드 가능하나, iOS(.ipa)·앱스토어 제출은 **macOS + Xcode** 가 필수다. 맥 한 대 또는 클라우드 맥 CI(Codemagic 등) + **Apple Developer Program($99/년)** 이 필요하다. 이 저장소는 iOS 코드베이스와 CI 설정까지 포함하되 실제 iOS 바이너리는 맥 환경에서 빌드한다. --- diff --git a/android/.gitignore b/android/.gitignore deleted file mode 100644 index bb2de31..0000000 --- a/android/.gitignore +++ /dev/null @@ -1,8 +0,0 @@ -.gradle/ -build/ -*/build/ -local.properties -.idea/ -*.iml -captures/ -.cxx/ diff --git a/android/build.gradle.kts b/android/build.gradle.kts deleted file mode 100644 index 749d414..0000000 --- a/android/build.gradle.kts +++ /dev/null @@ -1,3 +0,0 @@ -plugins { - kotlin("jvm") version "2.0.21" apply false -} diff --git a/android/engine/build.gradle.kts b/android/engine/build.gradle.kts deleted file mode 100644 index 93a15b4..0000000 --- a/android/engine/build.gradle.kts +++ /dev/null @@ -1,12 +0,0 @@ -plugins { - kotlin("jvm") -} - -dependencies { - testImplementation("junit:junit:4.13.2") -} - -tasks.test { - useJUnit() - testLogging { events("passed", "failed", "skipped") } -} diff --git a/android/engine/src/main/kotlin/kr/tkrmagid/tetrig/engine/Engine.kt b/android/engine/src/main/kotlin/kr/tkrmagid/tetrig/engine/Engine.kt deleted file mode 100644 index 29e68ed..0000000 --- a/android/engine/src/main/kotlin/kr/tkrmagid/tetrig/engine/Engine.kt +++ /dev/null @@ -1,352 +0,0 @@ -package kr.tkrmagid.tetrig.engine - -/** - * TETRIG 코어 엔진 (Kotlin 이식) — Phase 1. - * 순수 로직만: Android/렌더/타이머 의존 없음. UI 모듈이 타이밍을 구동한다. - * - * 좌표 규약 (명세서 §4.1): - * - 보드 (x, y): x 오른쪽 증가, y 위쪽 증가. y=0 이 맨 아래. - * - 킥 오프셋 (dx, dy): dy 양수 = 위쪽. 그대로 py += dy 로 적용. - * - 회전 상태: 0=스폰, 1=CW, 2=180, 3=CCW. - * - * (웹 레퍼런스 src/engine.js 와 동일 알고리즘·데이터를 Kotlin 으로 이식) - */ - -/** 튜닝 상수 (조정 대상은 여기 한 곳). */ -object Config { - const val BOARD_W = 10 - const val BOARD_H = 40 // 내부 높이 (하단 20행만 표시) - const val VISIBLE_H = 20 - const val NEXT_COUNT = 5 - const val LOCK_DELAY_MS = 500 - const val MAX_LOCK_RESETS = 15 - const val DEFAULT_GRAVITY_MS = 800 // Phase 1 기본 (SPRINT) - const val DAS_MS = 133 - const val ARR_MS = 10 - const val SDF = 20 -} - -val PIECE_TYPES = charArrayOf('I', 'O', 'T', 'S', 'Z', 'J', 'L') - -/** 피스 색 (명세서 §13 v2 확정). 'G' = 가비지. */ -val PIECE_COLORS: Map = mapOf( - 'I' to "#3EC1B6", 'O' to "#F5C531", 'T' to "#A76BF2", 'S' to "#7FCC4C", - 'Z' to "#F25C4C", 'J' to "#4C7DF2", 'L' to "#F2913D", 'G' to "#46464F", -) - -// ── 피스 스폰 셀 (박스-로컬, y 위쪽 증가) + 박스 크기 ── -private data class Spawn(val box: Int, val cells: Array) - -private val SPAWN: Map = mapOf( - 'I' to Spawn(4, arrayOf(intArrayOf(0, 2), intArrayOf(1, 2), intArrayOf(2, 2), intArrayOf(3, 2))), - 'O' to Spawn(2, arrayOf(intArrayOf(0, 0), intArrayOf(1, 0), intArrayOf(0, 1), intArrayOf(1, 1))), - 'T' to Spawn(3, arrayOf(intArrayOf(0, 1), intArrayOf(1, 1), intArrayOf(2, 1), intArrayOf(1, 2))), - 'S' to Spawn(3, arrayOf(intArrayOf(0, 1), intArrayOf(1, 1), intArrayOf(1, 2), intArrayOf(2, 2))), - 'Z' to Spawn(3, arrayOf(intArrayOf(0, 2), intArrayOf(1, 2), intArrayOf(1, 1), intArrayOf(2, 1))), - 'J' to Spawn(3, arrayOf(intArrayOf(0, 2), intArrayOf(0, 1), intArrayOf(1, 1), intArrayOf(2, 1))), - 'L' to Spawn(3, arrayOf(intArrayOf(2, 2), intArrayOf(0, 1), intArrayOf(1, 1), intArrayOf(2, 1))), -) - -/** 스폰 시 박스 원점의 보드 위치 (px, py). 셀은 (px+cx, py+cy). */ -private val SPAWN_POS: Map = mapOf( - 'I' to intArrayOf(3, 19), 'O' to intArrayOf(4, 21), 'T' to intArrayOf(3, 20), - 'S' to intArrayOf(3, 20), 'Z' to intArrayOf(3, 20), 'J' to intArrayOf(3, 20), 'L' to intArrayOf(3, 20), -) - -/** CW 회전: 박스 b 에서 (x,y) -> (y, (b-1)-x). */ -private fun rotateCW(cells: Array, b: Int): Array = - Array(cells.size) { i -> intArrayOf(cells[i][1], (b - 1) - cells[i][0]) } - -private fun buildStates(type: Char): Array> { - val sp = SPAWN.getValue(type) - if (type == 'O') return arrayOf(sp.cells, sp.cells, sp.cells, sp.cells) - val s0 = sp.cells - val s1 = rotateCW(s0, sp.box) - val s2 = rotateCW(s1, sp.box) - val s3 = rotateCW(s2, sp.box) - return arrayOf(s0, s1, s2, s3) -} - -/** STATES[type][state] = 셀 목록. */ -val STATES: Map>> = PIECE_TYPES.associateWith { buildStates(it) } - -// ── 킥테이블 (명세서 §4.1) ── -private val KICKS_JLSTZ: Map> = mapOf( - "0>1" to arrayOf(intArrayOf(0, 0), intArrayOf(-1, 0), intArrayOf(-1, 1), intArrayOf(0, -2), intArrayOf(-1, -2)), - "1>0" to arrayOf(intArrayOf(0, 0), intArrayOf(1, 0), intArrayOf(1, -1), intArrayOf(0, 2), intArrayOf(1, 2)), - "1>2" to arrayOf(intArrayOf(0, 0), intArrayOf(1, 0), intArrayOf(1, -1), intArrayOf(0, 2), intArrayOf(1, 2)), - "2>1" to arrayOf(intArrayOf(0, 0), intArrayOf(-1, 0), intArrayOf(-1, 1), intArrayOf(0, -2), intArrayOf(-1, -2)), - "2>3" to arrayOf(intArrayOf(0, 0), intArrayOf(1, 0), intArrayOf(1, 1), intArrayOf(0, -2), intArrayOf(1, -2)), - "3>2" to arrayOf(intArrayOf(0, 0), intArrayOf(-1, 0), intArrayOf(-1, -1), intArrayOf(0, 2), intArrayOf(-1, 2)), - "3>0" to arrayOf(intArrayOf(0, 0), intArrayOf(-1, 0), intArrayOf(-1, -1), intArrayOf(0, 2), intArrayOf(-1, 2)), - "0>3" to arrayOf(intArrayOf(0, 0), intArrayOf(1, 0), intArrayOf(1, 1), intArrayOf(0, -2), intArrayOf(1, -2)), - // 180° (SRS+) - "0>2" to arrayOf(intArrayOf(0, 0), intArrayOf(0, 1), intArrayOf(1, 1), intArrayOf(-1, 1), intArrayOf(1, 0), intArrayOf(-1, 0)), - "2>0" to arrayOf(intArrayOf(0, 0), intArrayOf(0, -1), intArrayOf(-1, -1), intArrayOf(1, -1), intArrayOf(-1, 0), intArrayOf(1, 0)), - "1>3" to arrayOf(intArrayOf(0, 0), intArrayOf(1, 0), intArrayOf(1, 2), intArrayOf(1, 1), intArrayOf(0, 2), intArrayOf(0, 1)), - "3>1" to arrayOf(intArrayOf(0, 0), intArrayOf(-1, 0), intArrayOf(-1, 2), intArrayOf(-1, 1), intArrayOf(0, 2), intArrayOf(0, 1)), -) - -private val KICKS_I: Map> = mapOf( - "0>1" to arrayOf(intArrayOf(0, 0), intArrayOf(-2, 0), intArrayOf(1, 0), intArrayOf(-2, -1), intArrayOf(1, 2)), - "1>0" to arrayOf(intArrayOf(0, 0), intArrayOf(2, 0), intArrayOf(-1, 0), intArrayOf(2, 1), intArrayOf(-1, -2)), - "1>2" to arrayOf(intArrayOf(0, 0), intArrayOf(-1, 0), intArrayOf(2, 0), intArrayOf(-1, 2), intArrayOf(2, -1)), - "2>1" to arrayOf(intArrayOf(0, 0), intArrayOf(1, 0), intArrayOf(-2, 0), intArrayOf(1, -2), intArrayOf(-2, 1)), - "2>3" to arrayOf(intArrayOf(0, 0), intArrayOf(2, 0), intArrayOf(-1, 0), intArrayOf(2, 1), intArrayOf(-1, -2)), - "3>2" to arrayOf(intArrayOf(0, 0), intArrayOf(-2, 0), intArrayOf(1, 0), intArrayOf(-2, -1), intArrayOf(1, 2)), - "3>0" to arrayOf(intArrayOf(0, 0), intArrayOf(1, 0), intArrayOf(-2, 0), intArrayOf(1, -2), intArrayOf(-2, 1)), - "0>3" to arrayOf(intArrayOf(0, 0), intArrayOf(-1, 0), intArrayOf(2, 0), intArrayOf(-1, 2), intArrayOf(2, -1)), - // I 180°는 (0,0)만 - "0>2" to arrayOf(intArrayOf(0, 0)), "2>0" to arrayOf(intArrayOf(0, 0)), - "1>3" to arrayOf(intArrayOf(0, 0)), "3>1" to arrayOf(intArrayOf(0, 0)), -) - -private val ORIGIN = arrayOf(intArrayOf(0, 0)) - -fun kickTable(type: Char, from: Int, to: Int): Array { - val key = "$from>$to" - return when (type) { - 'O' -> ORIGIN - 'I' -> KICKS_I[key] ?: ORIGIN - else -> KICKS_JLSTZ[key] ?: ORIGIN - } -} - -/** 시드 가능 난수 (mulberry32) — JS 판과 동일 알고리즘. */ -fun makeRng(seed: Int): () -> Double { - var a = seed - return { - a += 0x6D2B79F5 - var t = (a xor (a ushr 15)) * (1 or a) - t = (t + ((t xor (t ushr 7)) * (61 or t))) xor t - ((t xor (t ushr 14)).toLong() and 0xFFFFFFFFL).toDouble() / 4294967296.0 - } -} - -/** 7-bag. */ -class SevenBag(private val rng: () -> Double = { Math.random() }) { - private val q = ArrayDeque() - init { refill(); refill() } - private fun refill() { - val bag = PIECE_TYPES.toMutableList() - for (i in bag.indices.reversed()) { - val j = (rng() * (i + 1)).toInt() - val tmp = bag[i]; bag[i] = bag[j]; bag[j] = tmp - } - q.addAll(bag) - } - fun next(): Char { - if (q.size <= Config.NEXT_COUNT + 1) refill() - return q.removeFirst() - } - fun peek(n: Int = Config.NEXT_COUNT): List { - while (q.size < n) refill() - return q.take(n) - } -} - -// ── 보드 유틸 ── -typealias Board = Array> - -fun createBoard(w: Int = Config.BOARD_W, h: Int = Config.BOARD_H): Board = - Array(h) { arrayOfNulls(w) } - -fun pieceCells(type: Char, state: Int, px: Int, py: Int): Array { - val cells = STATES.getValue(type)[state] - return Array(cells.size) { i -> intArrayOf(px + cells[i][0], py + cells[i][1]) } -} - -/** x 범위 밖 / 바닥 아래(y<0) / 점유 = 충돌. y >= H (버퍼 위)는 허용. */ -fun collides(board: Board, cells: Array): Boolean { - val w = board[0].size; val h = board.size - for (c in cells) { - val x = c[0]; val y = c[1] - if (x < 0 || x >= w || y < 0) return true - if (y < h && board[y][x] != null) return true - } - return false -} - -/** 가득 찬 행 제거 후 위에서 빈 행 보충. 제거된 행 수 반환. */ -fun clearLines(board: Board): Int { - val w = board[0].size; val h = board.size - val rows = (0 until h).filter { y -> board[y].all { it != null } } - if (rows.isEmpty()) return 0 - val rowSet = rows.toHashSet() - val kept = ArrayList>((0 until h).filter { it !in rowSet }.map { board[it] }) - while (kept.size < h) kept.add(arrayOfNulls(w)) - for (y in 0 until h) board[y] = kept[y] - return rows.size -} - -fun isBoardEmpty(board: Board): Boolean = board.all { row -> row.all { it == null } } - -/** T-spin 판정 (명세서 §4.2). "none" | "mini" | "tspin". */ -fun detectTSpin(board: Board, type: Char, state: Int, px: Int, py: Int, lastMoveRotation: Boolean): String { - if (type != 'T' || !lastMoveRotation) return "none" - val w = board[0].size; val h = board.size - val cx = px + 1; val cy = py + 1 - fun filled(dx: Int, dy: Int): Boolean { - val x = cx + dx; val y = cy + dy - if (x < 0 || x >= w || y < 0) return true // 벽/바닥 = 채움 - if (y >= h) return false // 버퍼 위 = 빔 - return board[y][x] != null - } - val tl = filled(-1, 1); val tr = filled(1, 1); val bl = filled(-1, -1); val br = filled(1, -1) - val count = listOf(tl, tr, bl, br).count { it } - if (count < 3) return "none" - val front = when (state) { - 0 -> Pair(tl, tr) - 1 -> Pair(tr, br) - 2 -> Pair(bl, br) - else -> Pair(tl, bl) - } - return if (front.first && front.second) "tspin" else "mini" -} - -/** B2B 대상("어려운" 클리어): Quad 또는 라인 동반 T-spin 계열. */ -fun isDifficult(lines: Int, tspin: String): Boolean = - lines == 4 || (tspin != "none" && lines >= 1) - -data class LockResult( - val type: Char, val lines: Int, val tspin: String, val allClear: Boolean, - val combo: Int, val b2b: Int, val difficult: Boolean, -) - -class Active(var type: Char, var state: Int, var x: Int, var y: Int) - -/** 게임 상태 (UI 가 타이밍을 구동). */ -class TetrigGame(seed: Int = 1) { - var board: Board = createBoard() - private set - private val bag = SevenBag(makeRng(seed)) - var hold: Char? = null - private set - var holdUsed = false - private set - var over = false - private set - var lastMoveRotation = false - var lockResets = 0 - var combo = -1 - private set - var b2b = -1 - private set - var lastClear: LockResult? = null - private set - lateinit var active: Active - private set - - init { spawn() } - - fun cells(a: Active = active): Array = pieceCells(a.type, a.state, a.x, a.y) - - fun spawn(type: Char = bag.next()): Boolean { - val pos = SPAWN_POS.getValue(type) - active = Active(type, 0, pos[0], pos[1]) - lastMoveRotation = false - lockResets = 0 - if (collides(board, cells())) { over = true; return false } - return true - } - - fun peekNext(n: Int = Config.NEXT_COUNT): List = bag.peek(n) - - fun canFall(): Boolean { - val c = cells().map { intArrayOf(it[0], it[1] - 1) }.toTypedArray() - return !collides(board, c) - } - - fun resetLockDelay(): Boolean { - if (lockResets >= Config.MAX_LOCK_RESETS) return false - lockResets++ - return true - } - - fun move(dx: Int): Boolean { - if (over) return false - val c = pieceCells(active.type, active.state, active.x + dx, active.y) - if (collides(board, c)) return false - active.x += dx - lastMoveRotation = false - return true - } - - fun softDrop(): Boolean { - if (over) return false - val c = pieceCells(active.type, active.state, active.x, active.y - 1) - if (collides(board, c)) return false - active.y -= 1 - lastMoveRotation = false - return true - } - - /** dir: "cw" | "ccw" | "180". */ - fun rotate(dir: String): Boolean { - if (over) return false - val a = active - val to = when (dir) { - "cw" -> (a.state + 1) % 4 - "ccw" -> (a.state + 3) % 4 - else -> (a.state + 2) % 4 - } - for (k in kickTable(a.type, a.state, to)) { - val nx = a.x + k[0]; val ny = a.y + k[1] - if (!collides(board, pieceCells(a.type, to, nx, ny))) { - a.state = to; a.x = nx; a.y = ny - lastMoveRotation = true - return true - } - } - return false - } - - fun ghostCells(): Array { - val a = active - var dy = 0 - while (!collides(board, pieceCells(a.type, a.state, a.x, a.y - dy - 1))) dy++ - return pieceCells(a.type, a.state, a.x, a.y - dy) - } - - fun hardDrop(): LockResult? { - if (over) return null - val a = active - var d = 0 - while (!collides(board, pieceCells(a.type, a.state, a.x, a.y - d - 1))) d++ - a.y -= d - return lock() - } - - fun holdSwap(): Boolean { - if (over || holdUsed) return false - val cur = active.type - if (hold == null) { - hold = cur - spawn() - } else { - val swap = hold!! - hold = cur - spawn(swap) - } - holdUsed = true - return true - } - - fun lock(): LockResult { - val a = active - val tspin = detectTSpin(board, a.type, a.state, a.x, a.y, lastMoveRotation) - for (c in cells()) { - if (c[1] in board.indices) board[c[1]][c[0]] = a.type - } - val cleared = clearLines(board) - val allClear = cleared > 0 && isBoardEmpty(board) - combo = if (cleared > 0) combo + 1 else -1 - val diff = isDifficult(cleared, tspin) - if (cleared > 0) b2b = if (diff) b2b + 1 else -1 - val result = LockResult(a.type, cleared, tspin, allClear, combo, b2b, diff) - lastClear = result - holdUsed = false - spawn() - return result - } -} diff --git a/android/engine/src/test/kotlin/kr/tkrmagid/tetrig/engine/EngineTest.kt b/android/engine/src/test/kotlin/kr/tkrmagid/tetrig/engine/EngineTest.kt deleted file mode 100644 index 60e9cf0..0000000 --- a/android/engine/src/test/kotlin/kr/tkrmagid/tetrig/engine/EngineTest.kt +++ /dev/null @@ -1,204 +0,0 @@ -package kr.tkrmagid.tetrig.engine - -import org.junit.Assert.assertEquals -import org.junit.Assert.assertFalse -import org.junit.Assert.assertNotNull -import org.junit.Assert.assertTrue -import org.junit.Test - -class EngineTest { - - private fun cellSet(cells: Array): Set = - cells.map { "${it[0]},${it[1]}" }.toSet() - - // ── 피스 상태/회전 ── - @Test fun tSpawnCells() { - assertEquals(setOf("0,1", "1,1", "2,1", "1,2"), cellSet(STATES.getValue('T')[0])) - } - - @Test fun tCwState() { - // 오른쪽-포인팅: 세로줄 x=1 + (2,1) - assertEquals(setOf("1,0", "1,1", "1,2", "2,1"), cellSet(STATES.getValue('T')[1])) - } - - @Test fun iCwState() { - assertEquals(setOf("2,0", "2,1", "2,2", "2,3"), cellSet(STATES.getValue('I')[1])) - } - - @Test fun oNoRotation() { - val s0 = cellSet(STATES.getValue('O')[0]) - assertEquals(s0, cellSet(STATES.getValue('O')[1])) - assertEquals(s0, cellSet(STATES.getValue('O')[3])) - } - - @Test fun allPiecesHaveFourStates() { - for (t in PIECE_TYPES) assertEquals(4, STATES.getValue(t).size) - } - - // ── 킥테이블 ── - @Test fun kickTablesMatchSpec() { - assertEquals("0,0 -1,0 -1,1 0,-2 -1,-2", kickStr(kickTable('T', 0, 1))) - assertEquals("0,0 -2,0 1,0 -2,-1 1,2", kickStr(kickTable('I', 0, 1))) - assertEquals("0,0 0,1 1,1 -1,1 1,0 -1,0", kickStr(kickTable('T', 0, 2))) // 180 - assertEquals("0,0", kickStr(kickTable('I', 0, 2))) // I 180 - assertEquals("0,0", kickStr(kickTable('O', 0, 1))) - } - - private fun kickStr(k: Array) = k.joinToString(" ") { "${it[0]},${it[1]}" } - - @Test fun wallKickUsesOffsetWhenBlocked() { - val g = TetrigGame(5) - // 엔진의 board 를 비우고, active 를 직접 배치할 수 없으니 collides 로 시나리오만 확인 후 - // 실제 회전 킥은 별도 보드로 검증 - val board = createBoard() - // (0,0) 회전(state1) 셀 중 (5,5)를 막고, (-1,0) 킥으로 성사되는지 검증 - board[5][5] = 'X' - // state1 셀 at (4,5): pieceCells('T',1,4,5) - val blocked = collides(board, pieceCells('T', 1, 4, 5)) - assertTrue(blocked) - val kicked = collides(board, pieceCells('T', 1, 3, 5)) // (-1,0) - assertFalse(kicked) - } - - // ── 7-bag ── - @Test fun sevenBagContainsAllEachRound() { - val bag = SevenBag(makeRng(42)) - repeat(20) { - val seven = (0 until 7).map { bag.next() }.sorted() - assertEquals(PIECE_TYPES.toList().sorted(), seven) - } - } - - @Test fun peekMatchesConsumeOrder() { - val bag = SevenBag(makeRng(7)) - val peek = bag.peek(5) - assertEquals(5, peek.size) - for (i in 0 until 5) assertEquals(peek[i], bag.next()) - } - - @Test fun sameSeedSameSequence() { - val a = SevenBag(makeRng(123)); val b = SevenBag(makeRng(123)) - repeat(30) { assertEquals(a.next(), b.next()) } - } - - // ── 충돌/클리어 ── - @Test fun collidesWallsFloorOccupied() { - val bd = createBoard() - assertTrue(collides(bd, arrayOf(intArrayOf(-1, 0)))) - assertTrue(collides(bd, arrayOf(intArrayOf(10, 0)))) - assertTrue(collides(bd, arrayOf(intArrayOf(0, -1)))) - assertFalse(collides(bd, arrayOf(intArrayOf(5, 45)))) - bd[3][4] = 'T' - assertTrue(collides(bd, arrayOf(intArrayOf(4, 3)))) - } - - @Test fun clearLinesRemovesFullAndDrops() { - val bd = createBoard() - for (x in 0 until 10) bd[0][x] = 'I' - bd[1][0] = 'T' - assertEquals(1, clearLines(bd)) - assertEquals('T', bd[0][0]) - for (x in 1 until 10) assertEquals(null, bd[0][x]) - } - - @Test fun boardEmpty() { - val bd = createBoard() - assertTrue(isBoardEmpty(bd)) - bd[0][0] = 'I' - assertFalse(isBoardEmpty(bd)) - } - - // ── T-spin 판정 ── - @Test fun tspinFull() { - val bd = createBoard(10, 6) - bd[1][4] = 'X'; bd[1][6] = 'X'; bd[3][4] = 'X' - assertEquals("tspin", detectTSpin(bd, 'T', 2, 4, 1, true)) - } - - @Test fun tspinMini() { - val bd = createBoard(10, 6) - bd[1][4] = 'X'; bd[3][4] = 'X'; bd[3][6] = 'X' - assertEquals("mini", detectTSpin(bd, 'T', 2, 4, 1, true)) - } - - @Test fun tspinNoneTooFewCorners() { - val bd = createBoard(10, 6) - bd[1][4] = 'X' - assertEquals("none", detectTSpin(bd, 'T', 2, 4, 1, true)) - } - - @Test fun tspinNoneWhenNotRotation() { - val bd = createBoard(10, 6) - bd[1][4] = 'X'; bd[1][6] = 'X'; bd[3][4] = 'X' - assertEquals("none", detectTSpin(bd, 'T', 2, 4, 1, false)) - } - - @Test fun tspinFloorCountsAsFilled() { - val bd = createBoard(10, 6) - assertEquals("none", detectTSpin(bd, 'T', 2, 4, -1, true)) // 전면 2 + 뒤 0 = 2 - bd[1][4] = 'X' - assertEquals("tspin", detectTSpin(bd, 'T', 2, 4, -1, true)) // 3 + 전면 완전 - } - - @Test fun difficultRule() { - assertTrue(isDifficult(4, "none")) - assertTrue(isDifficult(2, "tspin")) - assertTrue(isDifficult(1, "mini")) - assertFalse(isDifficult(3, "none")) - assertFalse(isDifficult(0, "tspin")) - } - - // ── 게임 흐름 ── - @Test fun lockResetCappedAt15() { - val g = TetrigGame(1) - var ok = 0 - repeat(25) { if (g.resetLockDelay()) ok++ } - assertEquals(Config.MAX_LOCK_RESETS, ok) - } - - @Test fun spawnMoveHardDropCycle() { - val g = TetrigGame(3) - assertFalse(g.over) - val before = g.active.type - val res = g.hardDrop()!! - assertEquals(before, res.type) - assertNotNull(g.active) - assertFalse(g.holdUsed) - } - - @Test fun holdOncePerPiece() { - val g = TetrigGame(9) - val first = g.active.type - assertTrue(g.holdSwap()) - assertEquals(first, g.hold) - assertFalse(g.holdSwap()) - g.hardDrop() - assertFalse(g.holdUsed) - assertTrue(g.holdSwap()) - } - - @Test fun comboAndB2b() { - val g = TetrigGame(2) - fun quad(): LockResult { - for (y in 0 until 4) for (x in 1 until 10) g.board[y][x] = 'X' - // I 세로(state1)를 col0에 정렬: x=-2 → 셀 x=2-2=0 - val a = g.active - a.type = 'I'; a.state = 1; a.x = -2; a.y = 0 - g.lastMoveRotation = false - return g.hardDrop()!! - } - val r1 = quad() - assertEquals(4, r1.lines) - assertEquals(0, r1.b2b) - val r2 = quad() - assertEquals(4, r2.lines) - assertEquals(1, r2.b2b) - } - - @Test fun topOut() { - val g = TetrigGame(1) - for (y in 18 until 24) for (x in 0 until 10) g.board[y][x] = 'X' - assertFalse(g.spawn('T')) - assertTrue(g.over) - } -} diff --git a/android/gradle.properties b/android/gradle.properties deleted file mode 100644 index b430c7c..0000000 --- a/android/gradle.properties +++ /dev/null @@ -1,3 +0,0 @@ -org.gradle.jvmargs=-Xmx1536m -org.gradle.caching=true -kotlin.code.style=official diff --git a/android/gradle/wrapper/gradle-wrapper.jar b/android/gradle/wrapper/gradle-wrapper.jar deleted file mode 100644 index a4b76b9..0000000 Binary files a/android/gradle/wrapper/gradle-wrapper.jar and /dev/null differ diff --git a/android/gradlew b/android/gradlew deleted file mode 100755 index f5feea6..0000000 --- a/android/gradlew +++ /dev/null @@ -1,252 +0,0 @@ -#!/bin/sh - -# -# Copyright © 2015-2021 the original authors. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# https://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# SPDX-License-Identifier: Apache-2.0 -# - -############################################################################## -# -# Gradle start up script for POSIX generated by Gradle. -# -# Important for running: -# -# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is -# noncompliant, but you have some other compliant shell such as ksh or -# bash, then to run this script, type that shell name before the whole -# command line, like: -# -# ksh Gradle -# -# Busybox and similar reduced shells will NOT work, because this script -# requires all of these POSIX shell features: -# * functions; -# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», -# «${var#prefix}», «${var%suffix}», and «$( cmd )»; -# * compound commands having a testable exit status, especially «case»; -# * various built-in commands including «command», «set», and «ulimit». -# -# Important for patching: -# -# (2) This script targets any POSIX shell, so it avoids extensions provided -# by Bash, Ksh, etc; in particular arrays are avoided. -# -# The "traditional" practice of packing multiple parameters into a -# space-separated string is a well documented source of bugs and security -# problems, so this is (mostly) avoided, by progressively accumulating -# options in "$@", and eventually passing that to Java. -# -# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, -# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; -# see the in-line comments for details. -# -# There are tweaks for specific operating systems such as AIX, CygWin, -# Darwin, MinGW, and NonStop. -# -# (3) This script is generated from the Groovy template -# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt -# within the Gradle project. -# -# You can find Gradle at https://github.com/gradle/gradle/. -# -############################################################################## - -# Attempt to set APP_HOME - -# Resolve links: $0 may be a link -app_path=$0 - -# Need this for daisy-chained symlinks. -while - APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path - [ -h "$app_path" ] -do - ls=$( ls -ld "$app_path" ) - link=${ls#*' -> '} - case $link in #( - /*) app_path=$link ;; #( - *) app_path=$APP_HOME$link ;; - esac -done - -# This is normally unused -# shellcheck disable=SC2034 -APP_BASE_NAME=${0##*/} -# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) -APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s -' "$PWD" ) || exit - -# Use the maximum available, or set MAX_FD != -1 to use that value. -MAX_FD=maximum - -warn () { - echo "$*" -} >&2 - -die () { - echo - echo "$*" - echo - exit 1 -} >&2 - -# OS specific support (must be 'true' or 'false'). -cygwin=false -msys=false -darwin=false -nonstop=false -case "$( uname )" in #( - CYGWIN* ) cygwin=true ;; #( - Darwin* ) darwin=true ;; #( - MSYS* | MINGW* ) msys=true ;; #( - NONSTOP* ) nonstop=true ;; -esac - -CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar - - -# Determine the Java command to use to start the JVM. -if [ -n "$JAVA_HOME" ] ; then - if [ -x "$JAVA_HOME/jre/sh/java" ] ; then - # IBM's JDK on AIX uses strange locations for the executables - JAVACMD=$JAVA_HOME/jre/sh/java - else - JAVACMD=$JAVA_HOME/bin/java - fi - if [ ! -x "$JAVACMD" ] ; then - die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME - -Please set the JAVA_HOME variable in your environment to match the -location of your Java installation." - fi -else - JAVACMD=java - if ! command -v java >/dev/null 2>&1 - then - die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. - -Please set the JAVA_HOME variable in your environment to match the -location of your Java installation." - fi -fi - -# Increase the maximum file descriptors if we can. -if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then - case $MAX_FD in #( - max*) - # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. - # shellcheck disable=SC2039,SC3045 - MAX_FD=$( ulimit -H -n ) || - warn "Could not query maximum file descriptor limit" - esac - case $MAX_FD in #( - '' | soft) :;; #( - *) - # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. - # shellcheck disable=SC2039,SC3045 - ulimit -n "$MAX_FD" || - warn "Could not set maximum file descriptor limit to $MAX_FD" - esac -fi - -# Collect all arguments for the java command, stacking in reverse order: -# * args from the command line -# * the main class name -# * -classpath -# * -D...appname settings -# * --module-path (only if needed) -# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. - -# For Cygwin or MSYS, switch paths to Windows format before running java -if "$cygwin" || "$msys" ; then - APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) - CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) - - JAVACMD=$( cygpath --unix "$JAVACMD" ) - - # Now convert the arguments - kludge to limit ourselves to /bin/sh - for arg do - if - case $arg in #( - -*) false ;; # don't mess with options #( - /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath - [ -e "$t" ] ;; #( - *) false ;; - esac - then - arg=$( cygpath --path --ignore --mixed "$arg" ) - fi - # Roll the args list around exactly as many times as the number of - # args, so each arg winds up back in the position where it started, but - # possibly modified. - # - # NB: a `for` loop captures its iteration list before it begins, so - # changing the positional parameters here affects neither the number of - # iterations, nor the values presented in `arg`. - shift # remove old arg - set -- "$@" "$arg" # push replacement arg - done -fi - - -# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' - -# Collect all arguments for the java command: -# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, -# and any embedded shellness will be escaped. -# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be -# treated as '${Hostname}' itself on the command line. - -set -- \ - "-Dorg.gradle.appname=$APP_BASE_NAME" \ - -classpath "$CLASSPATH" \ - org.gradle.wrapper.GradleWrapperMain \ - "$@" - -# Stop when "xargs" is not available. -if ! command -v xargs >/dev/null 2>&1 -then - die "xargs is not available" -fi - -# Use "xargs" to parse quoted args. -# -# With -n1 it outputs one arg per line, with the quotes and backslashes removed. -# -# In Bash we could simply go: -# -# readarray ARGS < <( xargs -n1 <<<"$var" ) && -# set -- "${ARGS[@]}" "$@" -# -# but POSIX shell has neither arrays nor command substitution, so instead we -# post-process each arg (as a line of input to sed) to backslash-escape any -# character that might be a shell metacharacter, then use eval to reverse -# that process (while maintaining the separation between arguments), and wrap -# the whole thing up as a single "set" statement. -# -# This will of course break if any of these variables contains a newline or -# an unmatched quote. -# - -eval "set -- $( - printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | - xargs -n1 | - sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | - tr '\n' ' ' - )" '"$@"' - -exec "$JAVACMD" "$@" diff --git a/android/gradlew.bat b/android/gradlew.bat deleted file mode 100644 index 9b42019..0000000 --- a/android/gradlew.bat +++ /dev/null @@ -1,94 +0,0 @@ -@rem -@rem Copyright 2015 the original author or authors. -@rem -@rem Licensed under the Apache License, Version 2.0 (the "License"); -@rem you may not use this file except in compliance with the License. -@rem You may obtain a copy of the License at -@rem -@rem https://www.apache.org/licenses/LICENSE-2.0 -@rem -@rem Unless required by applicable law or agreed to in writing, software -@rem distributed under the License is distributed on an "AS IS" BASIS, -@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -@rem See the License for the specific language governing permissions and -@rem limitations under the License. -@rem -@rem SPDX-License-Identifier: Apache-2.0 -@rem - -@if "%DEBUG%"=="" @echo off -@rem ########################################################################## -@rem -@rem Gradle startup script for Windows -@rem -@rem ########################################################################## - -@rem Set local scope for the variables with windows NT shell -if "%OS%"=="Windows_NT" setlocal - -set DIRNAME=%~dp0 -if "%DIRNAME%"=="" set DIRNAME=. -@rem This is normally unused -set APP_BASE_NAME=%~n0 -set APP_HOME=%DIRNAME% - -@rem Resolve any "." and ".." in APP_HOME to make it shorter. -for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi - -@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" - -@rem Find java.exe -if defined JAVA_HOME goto findJavaFromJavaHome - -set JAVA_EXE=java.exe -%JAVA_EXE% -version >NUL 2>&1 -if %ERRORLEVEL% equ 0 goto execute - -echo. 1>&2 -echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 -echo. 1>&2 -echo Please set the JAVA_HOME variable in your environment to match the 1>&2 -echo location of your Java installation. 1>&2 - -goto fail - -:findJavaFromJavaHome -set JAVA_HOME=%JAVA_HOME:"=% -set JAVA_EXE=%JAVA_HOME%/bin/java.exe - -if exist "%JAVA_EXE%" goto execute - -echo. 1>&2 -echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 -echo. 1>&2 -echo Please set the JAVA_HOME variable in your environment to match the 1>&2 -echo location of your Java installation. 1>&2 - -goto fail - -:execute -@rem Setup the command line - -set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar - - -@rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* - -:end -@rem End local scope for the variables with windows NT shell -if %ERRORLEVEL% equ 0 goto mainEnd - -:fail -rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of -rem the _cmd.exe /c_ return code! -set EXIT_CODE=%ERRORLEVEL% -if %EXIT_CODE% equ 0 set EXIT_CODE=1 -if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% -exit /b %EXIT_CODE% - -:mainEnd -if "%OS%"=="Windows_NT" endlocal - -:omega diff --git a/android/settings.gradle.kts b/android/settings.gradle.kts deleted file mode 100644 index 4ab1901..0000000 --- a/android/settings.gradle.kts +++ /dev/null @@ -1,16 +0,0 @@ -pluginManagement { - repositories { - gradlePluginPortal() - google() - mavenCentral() - } -} -dependencyResolutionManagement { - repositories { - google() - mavenCentral() - } -} -rootProject.name = "tetrig" -include(":engine") -// :app (Jetpack Compose UI) 모듈은 UI 단계에서 추가한다. diff --git a/app/.gitignore b/app/.gitignore new file mode 100644 index 0000000..79f7eca --- /dev/null +++ b/app/.gitignore @@ -0,0 +1,48 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.build/ +.buildlog/ +.history +.svn/ +.swiftpm/ +migrate_working_dir/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +**/doc/api/ +**/ios/Flutter/.last_build_id +.dart_tool/ +.flutter-plugins-dependencies +.pub-cache/ +.pub/ +/build/ +/coverage/ + +# Symbolication related +app.*.symbols + +# Obfuscation related +app.*.map.json + +# Android Studio will place build artifacts here +/android/app/debug +/android/app/profile +/android/app/release + +# Widget Preview related +.widget_preview/ diff --git a/app/.metadata b/app/.metadata new file mode 100644 index 0000000..a069333 --- /dev/null +++ b/app/.metadata @@ -0,0 +1,33 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled and should not be manually edited. + +version: + revision: "6655482ec06e547f90abf8ae7590466f4415978d" + channel: "stable" + +project_type: app + +# Tracks metadata for the flutter migrate command +migration: + platforms: + - platform: root + create_revision: 6655482ec06e547f90abf8ae7590466f4415978d + base_revision: 6655482ec06e547f90abf8ae7590466f4415978d + - platform: android + create_revision: 6655482ec06e547f90abf8ae7590466f4415978d + base_revision: 6655482ec06e547f90abf8ae7590466f4415978d + - platform: ios + create_revision: 6655482ec06e547f90abf8ae7590466f4415978d + base_revision: 6655482ec06e547f90abf8ae7590466f4415978d + + # User provided section + + # List of Local paths (relative to this file) that should be + # ignored by the migrate tool. + # + # Files that are not part of the templates will be ignored by default. + unmanaged_files: + - 'lib/main.dart' + - 'ios/Runner.xcodeproj/project.pbxproj' diff --git a/app/README.md b/app/README.md new file mode 100644 index 0000000..ab9daaa --- /dev/null +++ b/app/README.md @@ -0,0 +1,17 @@ +# tetrig + +A new Flutter project. + +## Getting Started + +This project is a starting point for a Flutter application. + +A few resources to get you started if this is your first Flutter project: + +- [Learn Flutter](https://docs.flutter.dev/get-started/learn-flutter) +- [Write your first Flutter app](https://docs.flutter.dev/get-started/codelab) +- [Flutter learning resources](https://docs.flutter.dev/reference/learning-resources) + +For help getting started with Flutter development, view the +[online documentation](https://docs.flutter.dev/), which offers tutorials, +samples, guidance on mobile development, and a full API reference. diff --git a/app/analysis_options.yaml b/app/analysis_options.yaml new file mode 100644 index 0000000..cedcc10 --- /dev/null +++ b/app/analysis_options.yaml @@ -0,0 +1,38 @@ +# This file configures the analyzer, which statically analyzes Dart code to +# check for errors, warnings, and lints. +# +# The issues identified by the analyzer are surfaced in the UI of Dart-enabled +# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be +# invoked from the command line by running `flutter analyze`. + +# The following line activates a set of recommended lints for Flutter apps, +# packages, and plugins designed to encourage good coding practices. +include: package:flutter_lints/flutter.yaml + +analyzer: + exclude: + - build/** + - android/** + - ios/** + - web/** + - windows/** + - macos/** + - linux/** + +linter: + # The lint rules applied to this project can be customized in the + # section below to disable rules from the `package:flutter_lints/flutter.yaml` + # included above or to enable additional rules. A list of all available lints + # and their documentation is published at https://dart.dev/lints. + # + # Instead of disabling a lint rule for the entire project in the + # section below, it can also be suppressed for a single line of code + # or a specific dart file by using the `// ignore: name_of_lint` and + # `// ignore_for_file: name_of_lint` syntax on the line or in the file + # producing the lint. + rules: + # avoid_print: false # Uncomment to disable the `avoid_print` rule + # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule + +# Additional information about this file can be found at +# https://dart.dev/guides/language/analysis-options diff --git a/app/android/.gitignore b/app/android/.gitignore new file mode 100644 index 0000000..be3943c --- /dev/null +++ b/app/android/.gitignore @@ -0,0 +1,14 @@ +gradle-wrapper.jar +/.gradle +/captures/ +/gradlew +/gradlew.bat +/local.properties +GeneratedPluginRegistrant.java +.cxx/ + +# Remember to never publicly share your keystore. +# See https://flutter.dev/to/reference-keystore +key.properties +**/*.keystore +**/*.jks diff --git a/app/android/app/build.gradle.kts b/app/android/app/build.gradle.kts new file mode 100644 index 0000000..2e04cc9 --- /dev/null +++ b/app/android/app/build.gradle.kts @@ -0,0 +1,49 @@ +plugins { + id("com.android.application") + // The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins. + id("dev.flutter.flutter-gradle-plugin") +} + +android { + namespace = "kr.tkrmagid.tetrig" + compileSdk = flutter.compileSdkVersion + ndkVersion = flutter.ndkVersion + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 + } + + defaultConfig { + // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). + applicationId = "kr.tkrmagid.tetrig" + // You can update the following values to match your application needs. + // For more information, see: https://flutter.dev/to/review-gradle-config. + minSdk = flutter.minSdkVersion + targetSdk = flutter.targetSdkVersion + // Uses the version code from pubspec.yaml. When using split APKs, 1000 * ABI_VERSION + // is added automatically by Flutter. (https://developer.android.com/studio/build/configure-apk-splits#configure-APK-versions) + // You can force using the value of versionCode by specifying the `-P force-version-code-ignoring-abi=true` + // flag during build. + versionCode = flutter.versionCode + versionName = flutter.versionName + } + + buildTypes { + release { + // TODO: Add your own signing config for the release build. + // Signing with the debug keys for now, so `flutter run --release` works. + signingConfig = signingConfigs.getByName("debug") + } + } +} + +kotlin { + compilerOptions { + jvmTarget = org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17 + } +} + +flutter { + source = "../.." +} diff --git a/app/android/app/src/debug/AndroidManifest.xml b/app/android/app/src/debug/AndroidManifest.xml new file mode 100644 index 0000000..399f698 --- /dev/null +++ b/app/android/app/src/debug/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + diff --git a/app/android/app/src/main/AndroidManifest.xml b/app/android/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..acb7358 --- /dev/null +++ b/app/android/app/src/main/AndroidManifest.xml @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/app/android/app/src/main/kotlin/kr/tkrmagid/tetrig/MainActivity.kt b/app/android/app/src/main/kotlin/kr/tkrmagid/tetrig/MainActivity.kt new file mode 100644 index 0000000..7931963 --- /dev/null +++ b/app/android/app/src/main/kotlin/kr/tkrmagid/tetrig/MainActivity.kt @@ -0,0 +1,5 @@ +package kr.tkrmagid.tetrig + +import io.flutter.embedding.android.FlutterActivity + +class MainActivity : FlutterActivity() diff --git a/app/android/app/src/main/res/drawable-v21/launch_background.xml b/app/android/app/src/main/res/drawable-v21/launch_background.xml new file mode 100644 index 0000000..f74085f --- /dev/null +++ b/app/android/app/src/main/res/drawable-v21/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/app/android/app/src/main/res/drawable/launch_background.xml b/app/android/app/src/main/res/drawable/launch_background.xml new file mode 100644 index 0000000..304732f --- /dev/null +++ b/app/android/app/src/main/res/drawable/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/app/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/app/android/app/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000..db77bb4 Binary files /dev/null and b/app/android/app/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/app/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/app/android/app/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 0000000..17987b7 Binary files /dev/null and b/app/android/app/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/app/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/app/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 0000000..09d4391 Binary files /dev/null and b/app/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/app/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/app/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 0000000..d5f1c8d Binary files /dev/null and b/app/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/app/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/app/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 0000000..4d6372e Binary files /dev/null and b/app/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/app/android/app/src/main/res/values-night/styles.xml b/app/android/app/src/main/res/values-night/styles.xml new file mode 100644 index 0000000..06952be --- /dev/null +++ b/app/android/app/src/main/res/values-night/styles.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/app/android/app/src/main/res/values/styles.xml b/app/android/app/src/main/res/values/styles.xml new file mode 100644 index 0000000..cb1ef88 --- /dev/null +++ b/app/android/app/src/main/res/values/styles.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/app/android/app/src/profile/AndroidManifest.xml b/app/android/app/src/profile/AndroidManifest.xml new file mode 100644 index 0000000..399f698 --- /dev/null +++ b/app/android/app/src/profile/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + diff --git a/app/android/build.gradle.kts b/app/android/build.gradle.kts new file mode 100644 index 0000000..dbee657 --- /dev/null +++ b/app/android/build.gradle.kts @@ -0,0 +1,24 @@ +allprojects { + repositories { + google() + mavenCentral() + } +} + +val newBuildDir: Directory = + rootProject.layout.buildDirectory + .dir("../../build") + .get() +rootProject.layout.buildDirectory.value(newBuildDir) + +subprojects { + val newSubprojectBuildDir: Directory = newBuildDir.dir(project.name) + project.layout.buildDirectory.value(newSubprojectBuildDir) +} +subprojects { + project.evaluationDependsOn(":app") +} + +tasks.register("clean") { + delete(rootProject.layout.buildDirectory) +} diff --git a/app/android/gradle.properties b/app/android/gradle.properties new file mode 100644 index 0000000..e96108c --- /dev/null +++ b/app/android/gradle.properties @@ -0,0 +1,6 @@ +org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError +android.useAndroidX=true +# This newDsl flag was added by the Flutter template +android.newDsl=false +# This builtInKotlin flag was added by the Flutter template +android.builtInKotlin=false diff --git a/android/gradle/wrapper/gradle-wrapper.properties b/app/android/gradle/wrapper/gradle-wrapper.properties similarity index 73% rename from android/gradle/wrapper/gradle-wrapper.properties rename to app/android/gradle/wrapper/gradle-wrapper.properties index e2847c8..a20f2c4 100644 --- a/android/gradle/wrapper/gradle-wrapper.properties +++ b/app/android/gradle/wrapper/gradle-wrapper.properties @@ -1,7 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-bin.zip -networkTimeout=10000 -validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-9.3.1-all.zip diff --git a/app/android/settings.gradle.kts b/app/android/settings.gradle.kts new file mode 100644 index 0000000..b28021a --- /dev/null +++ b/app/android/settings.gradle.kts @@ -0,0 +1,26 @@ +pluginManagement { + val flutterSdkPath = + run { + val properties = java.util.Properties() + file("local.properties").inputStream().use { properties.load(it) } + val flutterSdkPath = properties.getProperty("flutter.sdk") + require(flutterSdkPath != null) { "flutter.sdk not set in local.properties" } + flutterSdkPath + } + + includeBuild("$flutterSdkPath/packages/flutter_tools/gradle") + + repositories { + google() + mavenCentral() + gradlePluginPortal() + } +} + +plugins { + id("dev.flutter.flutter-plugin-loader") version "1.0.0" + id("com.android.application") version "9.1.0" apply false + id("org.jetbrains.kotlin.android") version "2.4.0" apply false +} + +include(":app") diff --git a/app/ios/.gitignore b/app/ios/.gitignore new file mode 100644 index 0000000..7a7f987 --- /dev/null +++ b/app/ios/.gitignore @@ -0,0 +1,34 @@ +**/dgph +*.mode1v3 +*.mode2v3 +*.moved-aside +*.pbxuser +*.perspectivev3 +**/*sync/ +.sconsign.dblite +.tags* +**/.vagrant/ +**/DerivedData/ +Icon? +**/Pods/ +**/.symlinks/ +profile +xcuserdata +**/.generated/ +Flutter/App.framework +Flutter/Flutter.framework +Flutter/Flutter.podspec +Flutter/Generated.xcconfig +Flutter/ephemeral/ +Flutter/app.flx +Flutter/app.zip +Flutter/flutter_assets/ +Flutter/flutter_export_environment.sh +ServiceDefinitions.json +Runner/GeneratedPluginRegistrant.* + +# Exceptions to above rules. +!default.mode1v3 +!default.mode2v3 +!default.pbxuser +!default.perspectivev3 diff --git a/app/ios/Flutter/AppFrameworkInfo.plist b/app/ios/Flutter/AppFrameworkInfo.plist new file mode 100644 index 0000000..391a902 --- /dev/null +++ b/app/ios/Flutter/AppFrameworkInfo.plist @@ -0,0 +1,24 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + App + CFBundleIdentifier + io.flutter.flutter.app + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + App + CFBundlePackageType + FMWK + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1.0 + + diff --git a/app/ios/Flutter/Debug.xcconfig b/app/ios/Flutter/Debug.xcconfig new file mode 100644 index 0000000..592ceee --- /dev/null +++ b/app/ios/Flutter/Debug.xcconfig @@ -0,0 +1 @@ +#include "Generated.xcconfig" diff --git a/app/ios/Flutter/Release.xcconfig b/app/ios/Flutter/Release.xcconfig new file mode 100644 index 0000000..592ceee --- /dev/null +++ b/app/ios/Flutter/Release.xcconfig @@ -0,0 +1 @@ +#include "Generated.xcconfig" diff --git a/app/ios/Runner.xcodeproj/project.pbxproj b/app/ios/Runner.xcodeproj/project.pbxproj new file mode 100644 index 0000000..9d05f23 --- /dev/null +++ b/app/ios/Runner.xcodeproj/project.pbxproj @@ -0,0 +1,647 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 54; + objects = { + +/* Begin PBXBuildFile section */ + 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; + 331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; }; + 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; + 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; + 7884E8682EC3CC0700C636F2 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7884E8672EC3CC0400C636F2 /* SceneDelegate.swift */; }; + 78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */ = {isa = PBXBuildFile; productRef = 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */; }; + 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; + 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; + 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 331C8085294A63A400263BE5 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 97C146E61CF9000F007C117D /* Project object */; + proxyType = 1; + remoteGlobalIDString = 97C146ED1CF9000F007C117D; + remoteInfo = Runner; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 9705A1C41CF9048500538489 /* Embed Frameworks */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + ); + name = "Embed Frameworks"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; + 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; + 331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; }; + 331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; + 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; + 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 7884E8672EC3CC0400C636F2 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; + 78E0A7A72DC9AD7400C4905E /* FlutterGeneratedPluginSwiftPackage */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = FlutterGeneratedPluginSwiftPackage; path = Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage; sourceTree = ""; }; + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; + 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; + 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; + 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; + 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; + 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 97C146EB1CF9000F007C117D /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 331C8082294A63A400263BE5 /* RunnerTests */ = { + isa = PBXGroup; + children = ( + 331C807B294A618700263BE5 /* RunnerTests.swift */, + ); + path = RunnerTests; + sourceTree = ""; + }; + 9740EEB11CF90186004384FC /* Flutter */ = { + isa = PBXGroup; + children = ( + 78E0A7A72DC9AD7400C4905E /* FlutterGeneratedPluginSwiftPackage */, + 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, + 9740EEB21CF90195004384FC /* Debug.xcconfig */, + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, + 9740EEB31CF90195004384FC /* Generated.xcconfig */, + ); + name = Flutter; + sourceTree = ""; + }; + 97C146E51CF9000F007C117D = { + isa = PBXGroup; + children = ( + 9740EEB11CF90186004384FC /* Flutter */, + 97C146F01CF9000F007C117D /* Runner */, + 97C146EF1CF9000F007C117D /* Products */, + 331C8082294A63A400263BE5 /* RunnerTests */, + ); + sourceTree = ""; + }; + 97C146EF1CF9000F007C117D /* Products */ = { + isa = PBXGroup; + children = ( + 97C146EE1CF9000F007C117D /* Runner.app */, + 331C8081294A63A400263BE5 /* RunnerTests.xctest */, + ); + name = Products; + sourceTree = ""; + }; + 97C146F01CF9000F007C117D /* Runner */ = { + isa = PBXGroup; + children = ( + 97C146FA1CF9000F007C117D /* Main.storyboard */, + 97C146FD1CF9000F007C117D /* Assets.xcassets */, + 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */, + 97C147021CF9000F007C117D /* Info.plist */, + 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */, + 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */, + 74858FAE1ED2DC5600515810 /* AppDelegate.swift */, + 7884E8672EC3CC0400C636F2 /* SceneDelegate.swift */, + 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */, + ); + path = Runner; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 331C8080294A63A400263BE5 /* RunnerTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */; + buildPhases = ( + 331C807D294A63A400263BE5 /* Sources */, + 331C807F294A63A400263BE5 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 331C8086294A63A400263BE5 /* PBXTargetDependency */, + ); + name = RunnerTests; + productName = RunnerTests; + productReference = 331C8081294A63A400263BE5 /* RunnerTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; + 97C146ED1CF9000F007C117D /* Runner */ = { + isa = PBXNativeTarget; + buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; + buildPhases = ( + 9740EEB61CF901F6004384FC /* Run Script */, + 97C146EA1CF9000F007C117D /* Sources */, + 97C146EB1CF9000F007C117D /* Frameworks */, + 97C146EC1CF9000F007C117D /* Resources */, + 9705A1C41CF9048500538489 /* Embed Frameworks */, + 3B06AD1E1E4923F5004D2608 /* Thin Binary */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = Runner; + packageProductDependencies = ( + 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */, + ); + productName = Runner; + productReference = 97C146EE1CF9000F007C117D /* Runner.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 97C146E61CF9000F007C117D /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = YES; + LastUpgradeCheck = 1510; + ORGANIZATIONNAME = ""; + TargetAttributes = { + 331C8080294A63A400263BE5 = { + CreatedOnToolsVersion = 14.0; + TestTargetID = 97C146ED1CF9000F007C117D; + }; + 97C146ED1CF9000F007C117D = { + CreatedOnToolsVersion = 7.3.1; + LastSwiftMigration = 1100; + }; + }; + }; + buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 97C146E51CF9000F007C117D; + packageReferences = ( + 781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage" */, + ); + productRefGroup = 97C146EF1CF9000F007C117D /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 97C146ED1CF9000F007C117D /* Runner */, + 331C8080294A63A400263BE5 /* RunnerTests */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 331C807F294A63A400263BE5 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 97C146EC1CF9000F007C117D /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */, + 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */, + 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */, + 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "${TARGET_BUILD_DIR}/${INFOPLIST_PATH}", + ); + name = "Thin Binary"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; + }; + 9740EEB61CF901F6004384FC /* Run Script */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "Run Script"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 331C807D294A63A400263BE5 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 97C146EA1CF9000F007C117D /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */, + 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, + 7884E8682EC3CC0700C636F2 /* SceneDelegate.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 331C8086294A63A400263BE5 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 97C146ED1CF9000F007C117D /* Runner */; + targetProxy = 331C8085294A63A400263BE5 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin PBXVariantGroup section */ + 97C146FA1CF9000F007C117D /* Main.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 97C146FB1CF9000F007C117D /* Base */, + ); + name = Main.storyboard; + sourceTree = ""; + }; + 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 97C147001CF9000F007C117D /* Base */, + ); + name = LaunchScreen.storyboard; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 249021D3217E4FDB00AE95B9 /* Profile */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + STRING_CATALOG_GENERATE_SYMBOLS = YES; + SUPPORTED_PLATFORMS = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Profile; + }; + 249021D4217E4FDB00AE95B9 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = kr.tkrmagid.tetrig; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Profile; + }; + 331C8088294A63A400263BE5 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = kr.tkrmagid.tetrig.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; + }; + name = Debug; + }; + 331C8089294A63A400263BE5 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = kr.tkrmagid.tetrig.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; + }; + name = Release; + }; + 331C808A294A63A400263BE5 /* Profile */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = kr.tkrmagid.tetrig.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; + }; + name = Profile; + }; + 97C147031CF9000F007C117D /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + STRING_CATALOG_GENERATE_SYMBOLS = YES; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 97C147041CF9000F007C117D /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + STRING_CATALOG_GENERATE_SYMBOLS = YES; + SUPPORTED_PLATFORMS = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 97C147061CF9000F007C117D /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = kr.tkrmagid.tetrig; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Debug; + }; + 97C147071CF9000F007C117D /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = kr.tkrmagid.tetrig; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 331C8088294A63A400263BE5 /* Debug */, + 331C8089294A63A400263BE5 /* Release */, + 331C808A294A63A400263BE5 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 97C147031CF9000F007C117D /* Debug */, + 97C147041CF9000F007C117D /* Release */, + 249021D3217E4FDB00AE95B9 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 97C147061CF9000F007C117D /* Debug */, + 97C147071CF9000F007C117D /* Release */, + 249021D4217E4FDB00AE95B9 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + +/* Begin XCLocalSwiftPackageReference section */ + 781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage" */ = { + isa = XCLocalSwiftPackageReference; + relativePath = Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage; + }; +/* End XCLocalSwiftPackageReference section */ + +/* Begin XCSwiftPackageProductDependency section */ + 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */ = { + isa = XCSwiftPackageProductDependency; + productName = FlutterGeneratedPluginSwiftPackage; + }; +/* End XCSwiftPackageProductDependency section */ + }; + rootObject = 97C146E61CF9000F007C117D /* Project object */; +} diff --git a/app/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/app/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..919434a --- /dev/null +++ b/app/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/app/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/app/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/app/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/app/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/app/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 0000000..f9b0d7c --- /dev/null +++ b/app/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + PreviewsEnabled + + + diff --git a/app/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/app/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme new file mode 100644 index 0000000..c3fedb2 --- /dev/null +++ b/app/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -0,0 +1,119 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/ios/Runner.xcworkspace/contents.xcworkspacedata b/app/ios/Runner.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..1d526a1 --- /dev/null +++ b/app/ios/Runner.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/app/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/app/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/app/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/app/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/app/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 0000000..f9b0d7c --- /dev/null +++ b/app/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + PreviewsEnabled + + + diff --git a/app/ios/Runner/AppDelegate.swift b/app/ios/Runner/AppDelegate.swift new file mode 100644 index 0000000..c30b367 --- /dev/null +++ b/app/ios/Runner/AppDelegate.swift @@ -0,0 +1,16 @@ +import Flutter +import UIKit + +@main +@objc class AppDelegate: FlutterAppDelegate, FlutterImplicitEngineDelegate { + override func application( + _ application: UIApplication, + didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? + ) -> Bool { + return super.application(application, didFinishLaunchingWithOptions: launchOptions) + } + + func didInitializeImplicitFlutterEngine(_ engineBridge: FlutterImplicitEngineBridge) { + GeneratedPluginRegistrant.register(with: engineBridge.pluginRegistry) + } +} diff --git a/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..d36b1fa --- /dev/null +++ b/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,122 @@ +{ + "images" : [ + { + "size" : "20x20", + "idiom" : "iphone", + "filename" : "Icon-App-20x20@2x.png", + "scale" : "2x" + }, + { + "size" : "20x20", + "idiom" : "iphone", + "filename" : "Icon-App-20x20@3x.png", + "scale" : "3x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@1x.png", + "scale" : "1x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@2x.png", + "scale" : "2x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@3x.png", + "scale" : "3x" + }, + { + "size" : "40x40", + "idiom" : "iphone", + "filename" : "Icon-App-40x40@2x.png", + "scale" : "2x" + }, + { + "size" : "40x40", + "idiom" : "iphone", + "filename" : "Icon-App-40x40@3x.png", + "scale" : "3x" + }, + { + "size" : "60x60", + "idiom" : "iphone", + "filename" : "Icon-App-60x60@2x.png", + "scale" : "2x" + }, + { + "size" : "60x60", + "idiom" : "iphone", + "filename" : "Icon-App-60x60@3x.png", + "scale" : "3x" + }, + { + "size" : "20x20", + "idiom" : "ipad", + "filename" : "Icon-App-20x20@1x.png", + "scale" : "1x" + }, + { + "size" : "20x20", + "idiom" : "ipad", + "filename" : "Icon-App-20x20@2x.png", + "scale" : "2x" + }, + { + "size" : "29x29", + "idiom" : "ipad", + "filename" : "Icon-App-29x29@1x.png", + "scale" : "1x" + }, + { + "size" : "29x29", + "idiom" : "ipad", + "filename" : "Icon-App-29x29@2x.png", + "scale" : "2x" + }, + { + "size" : "40x40", + "idiom" : "ipad", + "filename" : "Icon-App-40x40@1x.png", + "scale" : "1x" + }, + { + "size" : "40x40", + "idiom" : "ipad", + "filename" : "Icon-App-40x40@2x.png", + "scale" : "2x" + }, + { + "size" : "76x76", + "idiom" : "ipad", + "filename" : "Icon-App-76x76@1x.png", + "scale" : "1x" + }, + { + "size" : "76x76", + "idiom" : "ipad", + "filename" : "Icon-App-76x76@2x.png", + "scale" : "2x" + }, + { + "size" : "83.5x83.5", + "idiom" : "ipad", + "filename" : "Icon-App-83.5x83.5@2x.png", + "scale" : "2x" + }, + { + "size" : "1024x1024", + "idiom" : "ios-marketing", + "filename" : "Icon-App-1024x1024@1x.png", + "scale" : "1x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png b/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png new file mode 100644 index 0000000..dc9ada4 Binary files /dev/null and b/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png differ diff --git a/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png b/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png new file mode 100644 index 0000000..7353c41 Binary files /dev/null and b/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png differ diff --git a/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png new file mode 100644 index 0000000..797d452 Binary files /dev/null and b/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png differ diff --git a/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png new file mode 100644 index 0000000..6ed2d93 Binary files /dev/null and b/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png differ diff --git a/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png new file mode 100644 index 0000000..4cd7b00 Binary files /dev/null and b/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png differ diff --git a/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png new file mode 100644 index 0000000..fe73094 Binary files /dev/null and b/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png differ diff --git a/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png b/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png new file mode 100644 index 0000000..321773c Binary files /dev/null and b/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png differ diff --git a/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png b/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png new file mode 100644 index 0000000..797d452 Binary files /dev/null and b/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png differ diff --git a/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png new file mode 100644 index 0000000..502f463 Binary files /dev/null and b/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png differ diff --git a/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png new file mode 100644 index 0000000..0ec3034 Binary files /dev/null and b/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png differ diff --git a/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png new file mode 100644 index 0000000..0ec3034 Binary files /dev/null and b/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png differ diff --git a/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png new file mode 100644 index 0000000..e9f5fea Binary files /dev/null and b/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png differ diff --git a/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png new file mode 100644 index 0000000..84ac32a Binary files /dev/null and b/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png differ diff --git a/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png b/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png new file mode 100644 index 0000000..8953cba Binary files /dev/null and b/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png differ diff --git a/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png new file mode 100644 index 0000000..0467bf1 Binary files /dev/null and b/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png differ diff --git a/app/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json b/app/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json new file mode 100644 index 0000000..0bedcf2 --- /dev/null +++ b/app/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "LaunchImage.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "LaunchImage@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "LaunchImage@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/app/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png b/app/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png new file mode 100644 index 0000000..9da19ea Binary files /dev/null and b/app/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png differ diff --git a/app/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png b/app/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png new file mode 100644 index 0000000..9da19ea Binary files /dev/null and b/app/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png differ diff --git a/app/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png b/app/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png new file mode 100644 index 0000000..9da19ea Binary files /dev/null and b/app/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png differ diff --git a/app/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md b/app/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md new file mode 100644 index 0000000..89c2725 --- /dev/null +++ b/app/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md @@ -0,0 +1,5 @@ +# Launch Screen Assets + +You can customize the launch screen with your own desired assets by replacing the image files in this directory. + +You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images. \ No newline at end of file diff --git a/app/ios/Runner/Base.lproj/LaunchScreen.storyboard b/app/ios/Runner/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 0000000..f2e259c --- /dev/null +++ b/app/ios/Runner/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/ios/Runner/Base.lproj/Main.storyboard b/app/ios/Runner/Base.lproj/Main.storyboard new file mode 100644 index 0000000..f3c2851 --- /dev/null +++ b/app/ios/Runner/Base.lproj/Main.storyboard @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/ios/Runner/Info.plist b/app/ios/Runner/Info.plist new file mode 100644 index 0000000..56ea290 --- /dev/null +++ b/app/ios/Runner/Info.plist @@ -0,0 +1,70 @@ + + + + + CADisableMinimumFrameDurationOnPhone + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleDisplayName + Tetrig + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + tetrig + CFBundlePackageType + APPL + CFBundleShortVersionString + $(FLUTTER_BUILD_NAME) + CFBundleSignature + ???? + CFBundleVersion + $(FLUTTER_BUILD_NUMBER) + LSRequiresIPhoneOS + + UIApplicationSceneManifest + + UIApplicationSupportsMultipleScenes + + UISceneConfigurations + + UIWindowSceneSessionRoleApplication + + + UISceneClassName + UIWindowScene + UISceneConfigurationName + flutter + UISceneDelegateClassName + $(PRODUCT_MODULE_NAME).SceneDelegate + UISceneStoryboardFile + Main + + + + + UIApplicationSupportsIndirectInputEvents + + UILaunchStoryboardName + LaunchScreen + UIMainStoryboardFile + Main + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + + diff --git a/app/ios/Runner/Runner-Bridging-Header.h b/app/ios/Runner/Runner-Bridging-Header.h new file mode 100644 index 0000000..308a2a5 --- /dev/null +++ b/app/ios/Runner/Runner-Bridging-Header.h @@ -0,0 +1 @@ +#import "GeneratedPluginRegistrant.h" diff --git a/app/ios/Runner/SceneDelegate.swift b/app/ios/Runner/SceneDelegate.swift new file mode 100644 index 0000000..b9ce8ea --- /dev/null +++ b/app/ios/Runner/SceneDelegate.swift @@ -0,0 +1,6 @@ +import Flutter +import UIKit + +class SceneDelegate: FlutterSceneDelegate { + +} diff --git a/app/ios/RunnerTests/RunnerTests.swift b/app/ios/RunnerTests/RunnerTests.swift new file mode 100644 index 0000000..86a7c3b --- /dev/null +++ b/app/ios/RunnerTests/RunnerTests.swift @@ -0,0 +1,12 @@ +import Flutter +import UIKit +import XCTest + +class RunnerTests: XCTestCase { + + func testExample() { + // If you add code to the Runner application, consider adding tests here. + // See https://developer.apple.com/documentation/xctest for more information about using XCTest. + } + +} diff --git a/app/lib/engine.dart b/app/lib/engine.dart new file mode 100644 index 0000000..9e2e5ba --- /dev/null +++ b/app/lib/engine.dart @@ -0,0 +1,403 @@ +/// TETRIG 코어 엔진 (Dart 이식) — Phase 1. +/// 순수 로직만: Flutter/렌더/타이머 의존 없음. UI 위젯이 타이밍을 구동한다. +/// +/// 좌표 규약 (명세서 §4.1): +/// - 보드 (x, y): x 오른쪽 증가, y 위쪽 증가. y=0 이 맨 아래. +/// - 킥 오프셋 (dx, dy): dy 양수 = 위쪽. 그대로 py += dy 로 적용. +/// - 회전 상태: 0=스폰, 1=CW, 2=180, 3=CCW. +/// +/// (웹 레퍼런스 src/engine.js, Kotlin 이식판과 동일 알고리즘·데이터) +library; + +/// 튜닝 상수 (조정 대상은 여기 한 곳). +class Config { + static const int boardW = 10; + static const int boardH = 40; // 내부 높이 (하단 20행만 표시) + static const int visibleH = 20; + static const int nextCount = 5; + static const int lockDelayMs = 500; + static const int maxLockResets = 15; + static const int defaultGravityMs = 800; // Phase 1 기본 (SPRINT) + static const int dasMs = 133; + static const int arrMs = 10; + static const int sdf = 20; +} + +const List pieceTypes = ['I', 'O', 'T', 'S', 'Z', 'J', 'L']; + +/// 피스 색 (명세서 §13 v2 확정). 'G' = 가비지. +const Map pieceColors = { + 'I': '#3EC1B6', 'O': '#F5C531', 'T': '#A76BF2', 'S': '#7FCC4C', + 'Z': '#F25C4C', 'J': '#4C7DF2', 'L': '#F2913D', 'G': '#46464F', +}; + +// ── 피스 스폰 셀 (박스-로컬, y 위쪽 증가) + 박스 크기 ── +class _Spawn { + final int box; + final List> cells; + const _Spawn(this.box, this.cells); +} + +const Map _spawn = { + 'I': _Spawn(4, [[0, 2], [1, 2], [2, 2], [3, 2]]), + 'O': _Spawn(2, [[0, 0], [1, 0], [0, 1], [1, 1]]), + 'T': _Spawn(3, [[0, 1], [1, 1], [2, 1], [1, 2]]), + 'S': _Spawn(3, [[0, 1], [1, 1], [1, 2], [2, 2]]), + 'Z': _Spawn(3, [[0, 2], [1, 2], [1, 1], [2, 1]]), + 'J': _Spawn(3, [[0, 2], [0, 1], [1, 1], [2, 1]]), + 'L': _Spawn(3, [[2, 2], [0, 1], [1, 1], [2, 1]]), +}; + +/// 스폰 시 박스 원점의 보드 위치 (px, py). +const Map> _spawnPos = { + 'I': [3, 19], 'O': [4, 21], 'T': [3, 20], + 'S': [3, 20], 'Z': [3, 20], 'J': [3, 20], 'L': [3, 20], +}; + +/// CW 회전: 박스 b 에서 (x,y) -> (y, (b-1)-x). +List> _rotateCW(List> cells, int b) => + cells.map((c) => [c[1], (b - 1) - c[0]]).toList(); + +List>> _buildStates(String type) { + final sp = _spawn[type]!; + if (type == 'O') return [sp.cells, sp.cells, sp.cells, sp.cells]; + final s0 = sp.cells; + final s1 = _rotateCW(s0, sp.box); + final s2 = _rotateCW(s1, sp.box); + final s3 = _rotateCW(s2, sp.box); + return [s0, s1, s2, s3]; +} + +/// STATES[type][state] = 셀 목록. +final Map>>> states = { + for (final t in pieceTypes) t: _buildStates(t), +}; + +// ── 킥테이블 (명세서 §4.1) ── +const Map>> _kicksJlstz = { + '0>1': [[0, 0], [-1, 0], [-1, 1], [0, -2], [-1, -2]], + '1>0': [[0, 0], [1, 0], [1, -1], [0, 2], [1, 2]], + '1>2': [[0, 0], [1, 0], [1, -1], [0, 2], [1, 2]], + '2>1': [[0, 0], [-1, 0], [-1, 1], [0, -2], [-1, -2]], + '2>3': [[0, 0], [1, 0], [1, 1], [0, -2], [1, -2]], + '3>2': [[0, 0], [-1, 0], [-1, -1], [0, 2], [-1, 2]], + '3>0': [[0, 0], [-1, 0], [-1, -1], [0, 2], [-1, 2]], + '0>3': [[0, 0], [1, 0], [1, 1], [0, -2], [1, -2]], + // 180° (SRS+) + '0>2': [[0, 0], [0, 1], [1, 1], [-1, 1], [1, 0], [-1, 0]], + '2>0': [[0, 0], [0, -1], [-1, -1], [1, -1], [-1, 0], [1, 0]], + '1>3': [[0, 0], [1, 0], [1, 2], [1, 1], [0, 2], [0, 1]], + '3>1': [[0, 0], [-1, 0], [-1, 2], [-1, 1], [0, 2], [0, 1]], +}; + +const Map>> _kicksI = { + '0>1': [[0, 0], [-2, 0], [1, 0], [-2, -1], [1, 2]], + '1>0': [[0, 0], [2, 0], [-1, 0], [2, 1], [-1, -2]], + '1>2': [[0, 0], [-1, 0], [2, 0], [-1, 2], [2, -1]], + '2>1': [[0, 0], [1, 0], [-2, 0], [1, -2], [-2, 1]], + '2>3': [[0, 0], [2, 0], [-1, 0], [2, 1], [-1, -2]], + '3>2': [[0, 0], [-2, 0], [1, 0], [-2, -1], [1, 2]], + '3>0': [[0, 0], [1, 0], [-2, 0], [1, -2], [-2, 1]], + '0>3': [[0, 0], [-1, 0], [2, 0], [-1, 2], [2, -1]], + // I 180°는 (0,0)만 + '0>2': [[0, 0]], '2>0': [[0, 0]], '1>3': [[0, 0]], '3>1': [[0, 0]], +}; + +const List> _origin = [[0, 0]]; + +List> kickTable(String type, int from, int to) { + final key = '$from>$to'; + if (type == 'O') return _origin; + if (type == 'I') return _kicksI[key] ?? _origin; + return _kicksJlstz[key] ?? _origin; +} + +/// 시드 가능 난수 (mulberry32). +double Function() makeRng(int seed) { + int a = seed & 0xFFFFFFFF; + return () { + a = (a + 0x6D2B79F5) & 0xFFFFFFFF; + final x = (a ^ (a >>> 15)) & 0xFFFFFFFF; + int t = (x * ((1 | a) & 0xFFFFFFFF)) & 0xFFFFFFFF; + final y = (t ^ (t >>> 7)) & 0xFFFFFFFF; + final m = (y * ((61 | t) & 0xFFFFFFFF)) & 0xFFFFFFFF; + t = ((t + m) & 0xFFFFFFFF) ^ t; + t &= 0xFFFFFFFF; + return ((t ^ (t >>> 14)) & 0xFFFFFFFF) / 4294967296.0; + }; +} + +/// 7-bag. +class SevenBag { + final double Function() rng; + final List _q = []; + SevenBag([double Function()? rng]) : rng = rng ?? (() => 0.0) { + _refill(); + _refill(); + } + void _refill() { + final bag = List.from(pieceTypes); + for (int i = bag.length - 1; i > 0; i--) { + final j = (rng() * (i + 1)).floor(); + final tmp = bag[i]; + bag[i] = bag[j]; + bag[j] = tmp; + } + _q.addAll(bag); + } + + String next() { + if (_q.length <= Config.nextCount + 1) _refill(); + return _q.removeAt(0); + } + + List peek([int n = Config.nextCount]) { + while (_q.length < n) { + _refill(); + } + return _q.sublist(0, n); + } +} + +// ── 보드 유틸 ── +List> createBoard([int w = Config.boardW, int h = Config.boardH]) => + List.generate(h, (_) => List.filled(w, null)); + +List> pieceCells(String type, int state, int px, int py) => + states[type]![state].map((c) => [px + c[0], py + c[1]]).toList(); + +/// x 범위 밖 / 바닥 아래(y<0) / 점유 = 충돌. y >= H (버퍼 위)는 허용. +bool collides(List> board, List> cells) { + final w = board[0].length, h = board.length; + for (final c in cells) { + final x = c[0], y = c[1]; + if (x < 0 || x >= w || y < 0) return true; + if (y < h && board[y][x] != null) return true; + } + return false; +} + +/// 가득 찬 행 제거 후 위에서 빈 행 보충. 제거된 행 수 반환. +int clearLines(List> board) { + final w = board[0].length, h = board.length; + final rows = []; + for (int y = 0; y < h; y++) { + if (board[y].every((c) => c != null)) rows.add(y); + } + if (rows.isEmpty) return 0; + final rowSet = rows.toSet(); + final kept = >[ + for (int y = 0; y < h; y++) + if (!rowSet.contains(y)) board[y] + ]; + while (kept.length < h) { + kept.add(List.filled(w, null)); + } + for (int y = 0; y < h; y++) { + board[y] = kept[y]; + } + return rows.length; +} + +bool isBoardEmpty(List> board) => + board.every((row) => row.every((c) => c == null)); + +/// T-spin 판정 (명세서 §4.2). 'none' | 'mini' | 'tspin'. +String detectTSpin(List> board, String type, int state, int px, + int py, bool lastMoveRotation) { + if (type != 'T' || !lastMoveRotation) return 'none'; + final w = board[0].length, h = board.length; + final cx = px + 1, cy = py + 1; + bool filled(int dx, int dy) { + final x = cx + dx, y = cy + dy; + if (x < 0 || x >= w || y < 0) return true; // 벽/바닥 = 채움 + if (y >= h) return false; // 버퍼 위 = 빔 + return board[y][x] != null; + } + + final tl = filled(-1, 1), tr = filled(1, 1), bl = filled(-1, -1), br = filled(1, -1); + final count = [tl, tr, bl, br].where((v) => v).length; + if (count < 3) return 'none'; + late List front; + switch (state) { + case 0: + front = [tl, tr]; + break; + case 1: + front = [tr, br]; + break; + case 2: + front = [bl, br]; + break; + default: + front = [tl, bl]; + } + return (front[0] && front[1]) ? 'tspin' : 'mini'; +} + +/// B2B 대상("어려운" 클리어): Quad 또는 라인 동반 T-spin 계열. +bool isDifficult(int lines, String tspin) => + lines == 4 || (tspin != 'none' && lines >= 1); + +class LockResult { + final String type; + final int lines; + final String tspin; + final bool allClear; + final int combo; + final int b2b; + final bool difficult; + const LockResult(this.type, this.lines, this.tspin, this.allClear, this.combo, + this.b2b, this.difficult); +} + +class Active { + String type; + int state; + int x; + int y; + Active(this.type, this.state, this.x, this.y); +} + +/// 게임 상태 (UI 가 타이밍을 구동). +class TetrigGame { + List> board = createBoard(); + final SevenBag _bag; + String? hold; + bool holdUsed = false; + bool over = false; + bool lastMoveRotation = false; + int lockResets = 0; + int combo = -1; + int b2b = -1; + LockResult? lastClear; + late Active active; + + TetrigGame({int seed = 1}) : _bag = SevenBag(makeRng(seed)) { + spawn(); + } + + List> cells([Active? a]) { + a ??= active; + return pieceCells(a.type, a.state, a.x, a.y); + } + + bool spawn([String? type]) { + type ??= _bag.next(); + final pos = _spawnPos[type]!; + active = Active(type, 0, pos[0], pos[1]); + lastMoveRotation = false; + lockResets = 0; + if (collides(board, cells())) { + over = true; + return false; + } + return true; + } + + List peekNext([int n = Config.nextCount]) => _bag.peek(n); + + bool canFall() { + final c = cells().map((p) => [p[0], p[1] - 1]).toList(); + return !collides(board, c); + } + + bool resetLockDelay() { + if (lockResets >= Config.maxLockResets) return false; + lockResets++; + return true; + } + + bool move(int dx) { + if (over) return false; + final c = pieceCells(active.type, active.state, active.x + dx, active.y); + if (collides(board, c)) return false; + active.x += dx; + lastMoveRotation = false; + return true; + } + + bool softDrop() { + if (over) return false; + final c = pieceCells(active.type, active.state, active.x, active.y - 1); + if (collides(board, c)) return false; + active.y -= 1; + lastMoveRotation = false; + return true; + } + + /// dir: 'cw' | 'ccw' | '180'. + bool rotate(String dir) { + if (over) return false; + final a = active; + final to = dir == 'cw' + ? (a.state + 1) % 4 + : dir == 'ccw' + ? (a.state + 3) % 4 + : (a.state + 2) % 4; + for (final k in kickTable(a.type, a.state, to)) { + final nx = a.x + k[0], ny = a.y + k[1]; + if (!collides(board, pieceCells(a.type, to, nx, ny))) { + a.state = to; + a.x = nx; + a.y = ny; + lastMoveRotation = true; + return true; + } + } + return false; + } + + List> ghostCells() { + final a = active; + int dy = 0; + while (!collides(board, pieceCells(a.type, a.state, a.x, a.y - dy - 1))) { + dy++; + } + return pieceCells(a.type, a.state, a.x, a.y - dy); + } + + LockResult? hardDrop() { + if (over) return null; + final a = active; + int d = 0; + while (!collides(board, pieceCells(a.type, a.state, a.x, a.y - d - 1))) { + d++; + } + a.y -= d; + return lock(); + } + + bool holdSwap() { + if (over || holdUsed) return false; + final cur = active.type; + if (hold == null) { + hold = cur; + spawn(); + } else { + final swap = hold!; + hold = cur; + spawn(swap); + } + holdUsed = true; + return true; + } + + LockResult lock() { + final a = active; + final tspin = detectTSpin(board, a.type, a.state, a.x, a.y, lastMoveRotation); + for (final c in cells()) { + if (c[1] >= 0 && c[1] < board.length) board[c[1]][c[0]] = a.type; + } + final cleared = clearLines(board); + final allClear = cleared > 0 && isBoardEmpty(board); + combo = cleared > 0 ? combo + 1 : -1; + final diff = isDifficult(cleared, tspin); + if (cleared > 0) b2b = diff ? b2b + 1 : -1; + final result = LockResult(a.type, cleared, tspin, allClear, combo, b2b, diff); + lastClear = result; + holdUsed = false; + spawn(); + return result; + } +} diff --git a/app/lib/main.dart b/app/lib/main.dart new file mode 100644 index 0000000..77c1995 --- /dev/null +++ b/app/lib/main.dart @@ -0,0 +1,34 @@ +import 'package:flutter/material.dart'; + +// TETRIG 진입점 (Flutter, Android + iOS). +// 게임 UI(목업 ② 인게임 + §11 터치 버튼)는 다음 단계에서 이 자리에 붙는다. +// 코어 로직은 lib/engine.dart (UI와 분리, Dart 테스트로 검증됨). + +void main() => runApp(const TetrigApp()); + +class TetrigApp extends StatelessWidget { + const TetrigApp({super.key}); + + @override + Widget build(BuildContext context) { + return const MaterialApp( + title: 'TETRIG', + debugShowCheckedModeBanner: false, + home: Scaffold( + backgroundColor: Color(0xFF17161B), // INK + body: Center( + child: Text( + 'TETRIG', + style: TextStyle( + color: Color(0xFFEDE7DA), // BONE + fontSize: 44, + fontWeight: FontWeight.w900, + fontStyle: FontStyle.italic, + letterSpacing: 1, + ), + ), + ), + ), + ); + } +} diff --git a/app/pubspec.lock b/app/pubspec.lock new file mode 100644 index 0000000..d6e40dc --- /dev/null +++ b/app/pubspec.lock @@ -0,0 +1,213 @@ +# Generated by pub +# See https://dart.dev/tools/pub/glossary#lockfile +packages: + async: + dependency: transitive + description: + name: async + sha256: e2eb0491ba5ddb6177742d2da23904574082139b07c1e33b8503b9f46f3e1a37 + url: "https://pub.dev" + source: hosted + version: "2.13.1" + boolean_selector: + dependency: transitive + description: + name: boolean_selector + sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea" + url: "https://pub.dev" + source: hosted + version: "2.1.2" + characters: + dependency: transitive + description: + name: characters + sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b + url: "https://pub.dev" + source: hosted + version: "1.4.1" + clock: + dependency: transitive + description: + name: clock + sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b + url: "https://pub.dev" + source: hosted + version: "1.1.2" + collection: + dependency: transitive + description: + name: collection + sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76" + url: "https://pub.dev" + source: hosted + version: "1.19.1" + cupertino_icons: + dependency: "direct main" + description: + name: cupertino_icons + sha256: "41e005c33bd814be4d3096aff55b1908d419fde52ca656c8c47719ec745873cd" + url: "https://pub.dev" + source: hosted + version: "1.0.9" + fake_async: + dependency: transitive + description: + name: fake_async + sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44" + url: "https://pub.dev" + source: hosted + version: "1.3.3" + flutter: + dependency: "direct main" + description: flutter + source: sdk + version: "0.0.0" + flutter_lints: + dependency: "direct dev" + description: + name: flutter_lints + sha256: "3105dc8492f6183fb076ccf1f351ac3d60564bff92e20bfc4af9cc1651f4e7e1" + url: "https://pub.dev" + source: hosted + version: "6.0.0" + flutter_test: + dependency: "direct dev" + description: flutter + source: sdk + version: "0.0.0" + leak_tracker: + dependency: transitive + description: + name: leak_tracker + sha256: "33e2e26bdd85a0112ec15400c8cbffea70d0f9c3407491f672a2fad47915e2de" + url: "https://pub.dev" + source: hosted + version: "11.0.2" + leak_tracker_flutter_testing: + dependency: transitive + description: + name: leak_tracker_flutter_testing + sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1" + url: "https://pub.dev" + source: hosted + version: "3.0.10" + leak_tracker_testing: + dependency: transitive + description: + name: leak_tracker_testing + sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1" + url: "https://pub.dev" + source: hosted + version: "3.0.2" + lints: + dependency: transitive + description: + name: lints + sha256: "12f842a479589fea194fe5c5a3095abc7be0c1f2ddfa9a0e76aed1dbd26a87df" + url: "https://pub.dev" + source: hosted + version: "6.1.0" + matcher: + dependency: transitive + description: + name: matcher + sha256: "31bd099b47c10cd1aeb55146a2d46ce0277630ecef3f7dae54ad7873f36696cd" + url: "https://pub.dev" + source: hosted + version: "0.12.20" + material_color_utilities: + dependency: transitive + description: + name: material_color_utilities + sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b" + url: "https://pub.dev" + source: hosted + version: "0.13.0" + meta: + dependency: transitive + description: + name: meta + sha256: c82594181e3312f3d0695fc95aaaf7758d75b8d4ae2bbecf223b9fd5109a059d + url: "https://pub.dev" + source: hosted + version: "1.18.3" + path: + dependency: transitive + description: + name: path + sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5" + url: "https://pub.dev" + source: hosted + version: "1.9.1" + sky_engine: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + source_span: + dependency: transitive + description: + name: source_span + sha256: "56a02f1f4cd1a2d96303c0144c93bd6d909eea6bee6bf5a0e0b685edbd4c47ab" + url: "https://pub.dev" + source: hosted + version: "1.10.2" + stack_trace: + dependency: transitive + description: + name: stack_trace + sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1" + url: "https://pub.dev" + source: hosted + version: "1.12.1" + stream_channel: + dependency: transitive + description: + name: stream_channel + sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d" + url: "https://pub.dev" + source: hosted + version: "2.1.4" + string_scanner: + dependency: transitive + description: + name: string_scanner + sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43" + url: "https://pub.dev" + source: hosted + version: "1.4.1" + term_glyph: + dependency: transitive + description: + name: term_glyph + sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e" + url: "https://pub.dev" + source: hosted + version: "1.2.2" + test_api: + dependency: transitive + description: + name: test_api + sha256: "2a122cbe059f8b610d3a5415f42e255b6c17b1f21eee1d960f31080237fb4f11" + url: "https://pub.dev" + source: hosted + version: "0.7.12" + vector_math: + dependency: transitive + description: + name: vector_math + sha256: "1d774bbdf6b72a0b12122fc1560c9c2d2a67db5a4a4cc2bd8a5c990ab20e3188" + url: "https://pub.dev" + source: hosted + version: "2.4.0" + vm_service: + dependency: transitive + description: + name: vm_service + sha256: "5f37239c4851efcef929cea7824e76df7f2f0970aef85d66bbc430afa40e72f0" + url: "https://pub.dev" + source: hosted + version: "15.3.0" +sdks: + dart: ">=3.13.1 <4.0.0" + flutter: ">=3.18.0-18.0.pre.54" diff --git a/app/pubspec.yaml b/app/pubspec.yaml new file mode 100644 index 0000000..571fe00 --- /dev/null +++ b/app/pubspec.yaml @@ -0,0 +1,89 @@ +name: tetrig +description: "A new Flutter project." +# The following line prevents the package from being accidentally published to +# pub.dev using `flutter pub publish`. This is preferred for private packages. +publish_to: 'none' # Remove this line if you wish to publish to pub.dev + +# The following defines the version and build number for your application. +# A version number is three numbers separated by dots, like 1.2.43 +# followed by an optional build number separated by a +. +# Both the version and the builder number may be overridden in flutter +# build by specifying --build-name and --build-number, respectively. +# In Android, build-name is used as versionName while build-number used as versionCode. +# Read more about Android versioning at https://developer.android.com/studio/publish/versioning +# In iOS, build-name is used as CFBundleShortVersionString while build-number is used as CFBundleVersion. +# Read more about iOS versioning at +# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html +# In Windows, build-name is used as the major, minor, and patch parts +# of the product and file versions while build-number is used as the build suffix. +version: 1.0.0+1 + +environment: + sdk: ^3.13.1 + +# Dependencies specify other packages that your package needs in order to work. +# To automatically upgrade your package dependencies to the latest versions +# consider running `flutter pub upgrade --major-versions`. Alternatively, +# dependencies can be manually updated by changing the version numbers below to +# the latest version available on pub.dev. To see which dependencies have newer +# versions available, run `flutter pub outdated`. +dependencies: + flutter: + sdk: flutter + + # The following adds the Cupertino Icons font to your application. + # Use with the CupertinoIcons class for iOS style icons. + cupertino_icons: ^1.0.8 + +dev_dependencies: + flutter_test: + sdk: flutter + + # The "flutter_lints" package below contains a set of recommended lints to + # encourage good coding practices. The lint set provided by the package is + # activated in the `analysis_options.yaml` file located at the root of your + # package. See that file for information about deactivating specific lint + # rules and activating additional ones. + flutter_lints: ^6.0.0 + +# For information on the generic Dart part of this file, see the +# following page: https://dart.dev/tools/pub/pubspec + +# The following section is specific to Flutter packages. +flutter: + + # The following line ensures that the Material Icons font is + # included with your application, so that you can use the icons in + # the material Icons class. + uses-material-design: true + + # To add assets to your application, add an assets section, like this: + # assets: + # - images/a_dot_burr.jpeg + # - images/a_dot_ham.jpeg + + # An image asset can refer to one or more resolution-specific "variants", see + # https://flutter.dev/to/resolution-aware-images + + # For details regarding adding assets from package dependencies, see + # https://flutter.dev/to/asset-from-package + + # To add custom fonts to your application, add a fonts section here, + # in this "flutter" section. Each entry in this list should have a + # "family" key with the font family name, and a "fonts" key with a + # list giving the asset and other descriptors for the font. For + # example: + # fonts: + # - family: Schyler + # fonts: + # - asset: fonts/Schyler-Regular.ttf + # - asset: fonts/Schyler-Italic.ttf + # style: italic + # - family: Trajan Pro + # fonts: + # - asset: fonts/TrajanPro.ttf + # - asset: fonts/TrajanPro_Bold.ttf + # weight: 700 + # + # For details regarding fonts from package dependencies, + # see https://flutter.dev/to/font-from-package diff --git a/app/test/engine_test.dart b/app/test/engine_test.dart new file mode 100644 index 0000000..10aa490 --- /dev/null +++ b/app/test/engine_test.dart @@ -0,0 +1,204 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:tetrig/engine.dart'; + +Set _cellSet(List> cells) => + cells.map((c) => '${c[0]},${c[1]}').toSet(); + +String _kickStr(List> k) => k.map((c) => '${c[0]},${c[1]}').join(' '); + +void main() { + // ── 피스 상태/회전 ── + test('T 스폰 셀 = 위-포인팅', () { + expect(_cellSet(states['T']![0]), {'0,1', '1,1', '2,1', '1,2'}); + }); + + test('T CW(state1) = 오른쪽-포인팅', () { + expect(_cellSet(states['T']![1]), {'1,0', '1,1', '1,2', '2,1'}); + }); + + test('I CW(state1) = 세로', () { + expect(_cellSet(states['I']![1]), {'2,0', '2,1', '2,2', '2,3'}); + }); + + test('O는 회전 불변', () { + final s0 = _cellSet(states['O']![0]); + expect(_cellSet(states['O']![1]), s0); + expect(_cellSet(states['O']![3]), s0); + }); + + test('모든 피스 4상태', () { + for (final t in pieceTypes) { + expect(states[t]!.length, 4); + } + }); + + // ── 킥테이블 ── + test('킥테이블 내용 (명세서 §4.1)', () { + expect(_kickStr(kickTable('T', 0, 1)), '0,0 -1,0 -1,1 0,-2 -1,-2'); + expect(_kickStr(kickTable('I', 0, 1)), '0,0 -2,0 1,0 -2,-1 1,2'); + expect(_kickStr(kickTable('T', 0, 2)), '0,0 0,1 1,1 -1,1 1,0 -1,0'); + expect(_kickStr(kickTable('I', 0, 2)), '0,0'); + expect(_kickStr(kickTable('O', 0, 1)), '0,0'); + }); + + test('벽킥: (0,0) 막히면 다음 오프셋으로 성사', () { + final board = createBoard(); + board[5][5] = 'X'; + expect(collides(board, pieceCells('T', 1, 4, 5)), true); + expect(collides(board, pieceCells('T', 1, 3, 5)), false); // (-1,0) + }); + + // ── 7-bag ── + test('7-bag: 연속 7개는 7종 모두', () { + final bag = SevenBag(makeRng(42)); + for (int r = 0; r < 20; r++) { + final seven = [for (int i = 0; i < 7; i++) bag.next()]..sort(); + expect(seven, (List.from(pieceTypes)..sort())); + } + }); + + test('peek(5)==소비순서', () { + final bag = SevenBag(makeRng(7)); + final peek = bag.peek(5); + expect(peek.length, 5); + for (int i = 0; i < 5; i++) { + expect(bag.next(), peek[i]); + } + }); + + test('시드 동일 → 동일 시퀀스', () { + final a = SevenBag(makeRng(123)), b = SevenBag(makeRng(123)); + for (int i = 0; i < 30; i++) { + expect(a.next(), b.next()); + } + }); + + // ── 충돌/클리어 ── + test('collides 벽/바닥/점유', () { + final bd = createBoard(); + expect(collides(bd, [[-1, 0]]), true); + expect(collides(bd, [[10, 0]]), true); + expect(collides(bd, [[0, -1]]), true); + expect(collides(bd, [[5, 45]]), false); + bd[3][4] = 'T'; + expect(collides(bd, [[4, 3]]), true); + }); + + test('clearLines 제거+하강', () { + final bd = createBoard(); + for (int x = 0; x < 10; x++) bd[0][x] = 'I'; + bd[1][0] = 'T'; + expect(clearLines(bd), 1); + expect(bd[0][0], 'T'); + for (int x = 1; x < 10; x++) expect(bd[0][x], null); + }); + + test('isBoardEmpty', () { + final bd = createBoard(); + expect(isBoardEmpty(bd), true); + bd[0][0] = 'I'; + expect(isBoardEmpty(bd), false); + }); + + // ── T-spin 판정 ── + test('T-spin full', () { + final bd = createBoard(10, 6); + bd[1][4] = 'X'; bd[1][6] = 'X'; bd[3][4] = 'X'; + expect(detectTSpin(bd, 'T', 2, 4, 1, true), 'tspin'); + }); + + test('T-spin mini', () { + final bd = createBoard(10, 6); + bd[1][4] = 'X'; bd[3][4] = 'X'; bd[3][6] = 'X'; + expect(detectTSpin(bd, 'T', 2, 4, 1, true), 'mini'); + }); + + test('T-spin none (모서리 부족)', () { + final bd = createBoard(10, 6); + bd[1][4] = 'X'; + expect(detectTSpin(bd, 'T', 2, 4, 1, true), 'none'); + }); + + test('T-spin none (회전 아님)', () { + final bd = createBoard(10, 6); + bd[1][4] = 'X'; bd[1][6] = 'X'; bd[3][4] = 'X'; + expect(detectTSpin(bd, 'T', 2, 4, 1, false), 'none'); + }); + + test('T-spin 바닥은 채움 취급', () { + final bd = createBoard(10, 6); + expect(detectTSpin(bd, 'T', 2, 4, -1, true), 'none'); + bd[1][4] = 'X'; + expect(detectTSpin(bd, 'T', 2, 4, -1, true), 'tspin'); + }); + + test('isDifficult 규칙', () { + expect(isDifficult(4, 'none'), true); + expect(isDifficult(2, 'tspin'), true); + expect(isDifficult(1, 'mini'), true); + expect(isDifficult(3, 'none'), false); + expect(isDifficult(0, 'tspin'), false); + }); + + // ── 게임 흐름 ── + test('락 딜레이 리셋 최대 15', () { + final g = TetrigGame(seed: 1); + int ok = 0; + for (int i = 0; i < 25; i++) { + if (g.resetLockDelay()) ok++; + } + expect(ok, Config.maxLockResets); + }); + + test('스폰/하드드롭 사이클', () { + final g = TetrigGame(seed: 3); + expect(g.over, false); + final before = g.active.type; + final res = g.hardDrop()!; + expect(res.type, before); + expect(g.holdUsed, false); + }); + + test('홀드 1회/피스', () { + final g = TetrigGame(seed: 9); + final first = g.active.type; + expect(g.holdSwap(), true); + expect(g.hold, first); + expect(g.holdSwap(), false); + g.hardDrop(); + expect(g.holdUsed, false); + expect(g.holdSwap(), true); + }); + + test('콤보/B2B 누적', () { + final g = TetrigGame(seed: 2); + LockResult quad() { + for (int y = 0; y < 4; y++) { + for (int x = 1; x < 10; x++) g.board[y][x] = 'X'; + } + final a = g.active; + a.type = 'I'; + a.state = 1; + a.x = -2; + a.y = 0; + g.lastMoveRotation = false; + return g.hardDrop()!; + } + + final r1 = quad(); + expect(r1.lines, 4); + expect(r1.b2b, 0); + final r2 = quad(); + expect(r2.lines, 4); + expect(r2.b2b, 1); + }); + + test('톱아웃', () { + final g = TetrigGame(seed: 1); + for (int y = 18; y < 24; y++) { + for (int x = 0; x < 10; x++) g.board[y][x] = 'X'; + } + expect(g.spawn('T'), false); + expect(g.over, true); + }); +}