Switch to Flutter (Android + iOS) for iPhone support; port engine to Dart

아이폰 지원 요구로 Android 전용 Kotlin 스택을 크로스플랫폼 Flutter로 교체.
- app/: Flutter 프로젝트(android+ios). 코어 로직은 순수 Dart 모듈 app/lib/engine.dart
  로 이식(SRS+180 킥·7-bag·홀드·고스트·락딜레이·T-spin/mini·콤보/B2B). 웹/Kotlin 판과 동일 알고리즘.
- app/test/engine_test.dart: Dart 테스트 24개 전부 통과, flutter analyze 0 issue.
- 이전 android/(Kotlin) 모듈 제거(Flutter로 대체). README에 Flutter 전환 + iOS 빌드 제약(맥 필요) 명시.
- 게임 UI(목업 ② + §11 터치)와 설치용 빌드는 다음 단계.
This commit is contained in:
tkrmagid
2026-08-22 14:40:03 +09:00
parent 9a41c9a340
commit cbb8dea2eb
79 changed files with 2516 additions and 948 deletions

View File

@@ -6,7 +6,9 @@ TETR.IO 스타일의 **모바일 실시간 대전 블록 스태커** 웹 게임.
이 문서는 앱 제작을 위한 **최종 빌드 진입점(entry point)** 이다. 모든 설계 결정·수치·에셋의 **정본(source of truth)은 [`TETRIG-최종명세서.md`](./TETRIG-최종명세서.md)** 이며, 구현 시 반드시 명세서와 UX 목업을 함께 열어 작업한다. 이 문서는 앱 제작을 위한 **최종 빌드 진입점(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 바이너리는 맥 환경에서 빌드한다.
--- ---

8
android/.gitignore vendored
View File

@@ -1,8 +0,0 @@
.gradle/
build/
*/build/
local.properties
.idea/
*.iml
captures/
.cxx/

View File

@@ -1,3 +0,0 @@
plugins {
kotlin("jvm") version "2.0.21" apply false
}

View File

@@ -1,12 +0,0 @@
plugins {
kotlin("jvm")
}
dependencies {
testImplementation("junit:junit:4.13.2")
}
tasks.test {
useJUnit()
testLogging { events("passed", "failed", "skipped") }
}

View File

@@ -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<Char, String> = 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<IntArray>)
private val SPAWN: Map<Char, Spawn> = 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<Char, IntArray> = 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<IntArray>, b: Int): Array<IntArray> =
Array(cells.size) { i -> intArrayOf(cells[i][1], (b - 1) - cells[i][0]) }
private fun buildStates(type: Char): Array<Array<IntArray>> {
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<Char, Array<Array<IntArray>>> = PIECE_TYPES.associateWith { buildStates(it) }
// ── 킥테이블 (명세서 §4.1) ──
private val KICKS_JLSTZ: Map<String, Array<IntArray>> = 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<String, Array<IntArray>> = 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<IntArray> {
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<Char>()
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<Char> {
while (q.size < n) refill()
return q.take(n)
}
}
// ── 보드 유틸 ──
typealias Board = Array<Array<Char?>>
fun createBoard(w: Int = Config.BOARD_W, h: Int = Config.BOARD_H): Board =
Array(h) { arrayOfNulls<Char>(w) }
fun pieceCells(type: Char, state: Int, px: Int, py: Int): Array<IntArray> {
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<IntArray>): 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<Array<Char?>>((0 until h).filter { it !in rowSet }.map { board[it] })
while (kept.size < h) kept.add(arrayOfNulls<Char>(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<IntArray> = 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<Char> = 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<IntArray> {
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
}
}

View File

@@ -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<IntArray>): Set<String> =
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<IntArray>) = 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)
}
}

View File

@@ -1,3 +0,0 @@
org.gradle.jvmargs=-Xmx1536m
org.gradle.caching=true
kotlin.code.style=official

Binary file not shown.

252
android/gradlew vendored
View File

@@ -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" "$@"

94
android/gradlew.bat vendored
View File

@@ -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

View File

@@ -1,16 +0,0 @@
pluginManagement {
repositories {
gradlePluginPortal()
google()
mavenCentral()
}
}
dependencyResolutionManagement {
repositories {
google()
mavenCentral()
}
}
rootProject.name = "tetrig"
include(":engine")
// :app (Jetpack Compose UI) 모듈은 UI 단계에서 추가한다.

48
app/.gitignore vendored Normal file
View File

@@ -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/

33
app/.metadata Normal file
View File

@@ -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'

17
app/README.md Normal file
View File

@@ -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.

38
app/analysis_options.yaml Normal file
View File

@@ -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

14
app/android/.gitignore vendored Normal file
View File

@@ -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

View File

@@ -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 = "../.."
}

View File

@@ -0,0 +1,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- The INTERNET permission is required for development. Specifically,
the Flutter tool needs it to communicate with the running application
to allow setting breakpoints, to provide hot reload, etc.
-->
<uses-permission android:name="android.permission.INTERNET"/>
</manifest>

View File

@@ -0,0 +1,45 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application
android:label="tetrig"
android:name="${applicationName}"
android:icon="@mipmap/ic_launcher">
<activity
android:name=".MainActivity"
android:exported="true"
android:launchMode="singleTop"
android:taskAffinity=""
android:theme="@style/LaunchTheme"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:hardwareAccelerated="true"
android:windowSoftInputMode="adjustResize">
<!-- Specifies an Android theme to apply to this Activity as soon as
the Android process has started. This theme is visible to the user
while the Flutter UI initializes. After that, this theme continues
to determine the Window background behind the Flutter UI. -->
<meta-data
android:name="io.flutter.embedding.android.NormalTheme"
android:resource="@style/NormalTheme"
/>
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
<!-- Don't delete the meta-data below.
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
<meta-data
android:name="flutterEmbedding"
android:value="2" />
</application>
<!-- Required to query activities that can process text, see:
https://developer.android.com/training/package-visibility and
https://developer.android.com/reference/android/content/Intent#ACTION_PROCESS_TEXT.
In particular, this is used by the Flutter engine in io.flutter.plugin.text.ProcessTextPlugin. -->
<queries>
<intent>
<action android:name="android.intent.action.PROCESS_TEXT"/>
<data android:mimeType="text/plain"/>
</intent>
</queries>
</manifest>

View File

@@ -0,0 +1,5 @@
package kr.tkrmagid.tetrig
import io.flutter.embedding.android.FlutterActivity
class MainActivity : FlutterActivity()

View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Modify this file to customize your launch splash screen -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="?android:colorBackground" />
<!-- You can insert your own image assets here -->
<!-- <item>
<bitmap
android:gravity="center"
android:src="@mipmap/launch_image" />
</item> -->
</layer-list>

View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Modify this file to customize your launch splash screen -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@android:color/white" />
<!-- You can insert your own image assets here -->
<!-- <item>
<bitmap
android:gravity="center"
android:src="@mipmap/launch_image" />
</item> -->
</layer-list>

Binary file not shown.

After

Width:  |  Height:  |  Size: 544 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 442 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 721 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is on -->
<style name="LaunchTheme" parent="@android:style/Theme.Black.NoTitleBar">
<!-- Show a splash screen on the activity. Automatically removed when
the Flutter engine draws its first frame -->
<item name="android:windowBackground">@drawable/launch_background</item>
</style>
<!-- Theme applied to the Android Window as soon as the process has started.
This theme determines the color of the Android Window while your
Flutter UI initializes, as well as behind your Flutter UI while its
running.
This Theme is only used starting with V2 of Flutter's Android embedding. -->
<style name="NormalTheme" parent="@android:style/Theme.Black.NoTitleBar">
<item name="android:windowBackground">?android:colorBackground</item>
</style>
</resources>

View File

@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is off -->
<style name="LaunchTheme" parent="@android:style/Theme.Light.NoTitleBar">
<!-- Show a splash screen on the activity. Automatically removed when
the Flutter engine draws its first frame -->
<item name="android:windowBackground">@drawable/launch_background</item>
</style>
<!-- Theme applied to the Android Window as soon as the process has started.
This theme determines the color of the Android Window while your
Flutter UI initializes, as well as behind your Flutter UI while its
running.
This Theme is only used starting with V2 of Flutter's Android embedding. -->
<style name="NormalTheme" parent="@android:style/Theme.Light.NoTitleBar">
<item name="android:windowBackground">?android:colorBackground</item>
</style>
</resources>

View File

@@ -0,0 +1,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- The INTERNET permission is required for development. Specifically,
the Flutter tool needs it to communicate with the running application
to allow setting breakpoints, to provide hot reload, etc.
-->
<uses-permission android:name="android.permission.INTERNET"/>
</manifest>

View File

@@ -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<Delete>("clean") {
delete(rootProject.layout.buildDirectory)
}

View File

@@ -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

View File

@@ -1,7 +1,5 @@
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-9.3.1-all.zip

View File

@@ -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")

34
app/ios/.gitignore vendored Normal file
View File

@@ -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

View File

@@ -0,0 +1,24 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>en</string>
<key>CFBundleExecutable</key>
<string>App</string>
<key>CFBundleIdentifier</key>
<string>io.flutter.flutter.app</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>App</string>
<key>CFBundlePackageType</key>
<string>FMWK</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>1.0</string>
</dict>
</plist>

View File

@@ -0,0 +1 @@
#include "Generated.xcconfig"

View File

@@ -0,0 +1 @@
#include "Generated.xcconfig"

View File

@@ -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 = "<group>"; };
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = "<group>"; };
331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = "<group>"; };
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 = "<group>"; };
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = "<group>"; };
74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
7884E8672EC3CC0400C636F2 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = "<group>"; };
78E0A7A72DC9AD7400C4905E /* FlutterGeneratedPluginSwiftPackage */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = FlutterGeneratedPluginSwiftPackage; path = Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage; sourceTree = "<group>"; };
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = "<group>"; };
9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = "<group>"; };
9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = "<group>"; };
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 = "<group>"; };
97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
/* 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 = "<group>";
};
9740EEB11CF90186004384FC /* Flutter */ = {
isa = PBXGroup;
children = (
78E0A7A72DC9AD7400C4905E /* FlutterGeneratedPluginSwiftPackage */,
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */,
9740EEB21CF90195004384FC /* Debug.xcconfig */,
7AFA3C8E1D35360C0083082E /* Release.xcconfig */,
9740EEB31CF90195004384FC /* Generated.xcconfig */,
);
name = Flutter;
sourceTree = "<group>";
};
97C146E51CF9000F007C117D = {
isa = PBXGroup;
children = (
9740EEB11CF90186004384FC /* Flutter */,
97C146F01CF9000F007C117D /* Runner */,
97C146EF1CF9000F007C117D /* Products */,
331C8082294A63A400263BE5 /* RunnerTests */,
);
sourceTree = "<group>";
};
97C146EF1CF9000F007C117D /* Products */ = {
isa = PBXGroup;
children = (
97C146EE1CF9000F007C117D /* Runner.app */,
331C8081294A63A400263BE5 /* RunnerTests.xctest */,
);
name = Products;
sourceTree = "<group>";
};
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 = "<group>";
};
/* 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 = "<group>";
};
97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = {
isa = PBXVariantGroup;
children = (
97C147001CF9000F007C117D /* Base */,
);
name = LaunchScreen.storyboard;
sourceTree = "<group>";
};
/* 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 */;
}

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "self:">
</FileRef>
</Workspace>

View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>IDEDidComputeMac32BitWarning</key>
<true/>
</dict>
</plist>

View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>PreviewsEnabled</key>
<false/>
</dict>
</plist>

View File

@@ -0,0 +1,119 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1510"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
<PreActions>
<ExecutionAction
ActionType = "Xcode.IDEStandardExecutionActionsCore.ExecutionActionType.ShellScriptAction">
<ActionContent
title = "Run Prepare Flutter Framework Script"
scriptText = "/bin/sh &quot;$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh&quot; prepare&#10;">
<EnvironmentBuildable>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</EnvironmentBuildable>
</ActionContent>
</ExecutionAction>
</PreActions>
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
customLLDBInitFile = "$(SRCROOT)/Flutter/ephemeral/flutter_lldbinit"
shouldUseLaunchSchemeArgsEnv = "YES">
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</MacroExpansion>
<Testables>
<TestableReference
skipped = "NO"
parallelizable = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "331C8080294A63A400263BE5"
BuildableName = "RunnerTests.xctest"
BlueprintName = "RunnerTests"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</TestableReference>
</Testables>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
customLLDBInitFile = "$(SRCROOT)/Flutter/ephemeral/flutter_lldbinit"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
enableGPUValidationMode = "1"
allowLocationSimulation = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</LaunchAction>
<ProfileAction
buildConfiguration = "Profile"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "group:Runner.xcodeproj">
</FileRef>
</Workspace>

View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>IDEDidComputeMac32BitWarning</key>
<true/>
</dict>
</plist>

View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>PreviewsEnabled</key>
<false/>
</dict>
</plist>

View File

@@ -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)
}
}

View File

@@ -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"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 295 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 406 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 450 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 282 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 462 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 704 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 406 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 586 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 862 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 862 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 762 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@@ -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"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 B

View File

@@ -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.

View File

@@ -0,0 +1,37 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="12121" systemVersion="16G29" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" colorMatched="YES" initialViewController="01J-lp-oVM">
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="12089"/>
</dependencies>
<scenes>
<!--View Controller-->
<scene sceneID="EHf-IW-A2E">
<objects>
<viewController id="01J-lp-oVM" sceneMemberID="viewController">
<layoutGuides>
<viewControllerLayoutGuide type="top" id="Ydg-fD-yQy"/>
<viewControllerLayoutGuide type="bottom" id="xbc-2k-c8Z"/>
</layoutGuides>
<view key="view" contentMode="scaleToFill" id="Ze5-6b-2t3">
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<imageView opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" image="LaunchImage" translatesAutoresizingMaskIntoConstraints="NO" id="YRO-k0-Ey4">
</imageView>
</subviews>
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<constraints>
<constraint firstItem="YRO-k0-Ey4" firstAttribute="centerX" secondItem="Ze5-6b-2t3" secondAttribute="centerX" id="1a2-6s-vTC"/>
<constraint firstItem="YRO-k0-Ey4" firstAttribute="centerY" secondItem="Ze5-6b-2t3" secondAttribute="centerY" id="4X2-HB-R7a"/>
</constraints>
</view>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="iYj-Kq-Ea1" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="53" y="375"/>
</scene>
</scenes>
<resources>
<image name="LaunchImage" width="168" height="185"/>
</resources>
</document>

View File

@@ -0,0 +1,26 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="10117" systemVersion="15F34" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" initialViewController="BYZ-38-t0r">
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="10085"/>
</dependencies>
<scenes>
<!--Flutter View Controller-->
<scene sceneID="tne-QT-ifu">
<objects>
<viewController id="BYZ-38-t0r" customClass="FlutterViewController" sceneMemberID="viewController">
<layoutGuides>
<viewControllerLayoutGuide type="top" id="y3c-jy-aDJ"/>
<viewControllerLayoutGuide type="bottom" id="wfy-db-euE"/>
</layoutGuides>
<view key="view" contentMode="scaleToFill" id="8bC-Xf-vdC">
<rect key="frame" x="0.0" y="0.0" width="600" height="600"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="calibratedWhite"/>
</view>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="dkx-z0-nzr" sceneMemberID="firstResponder"/>
</objects>
</scene>
</scenes>
</document>

70
app/ios/Runner/Info.plist Normal file
View File

@@ -0,0 +1,70 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CADisableMinimumFrameDurationOnPhone</key>
<true/>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleDisplayName</key>
<string>Tetrig</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>tetrig</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>$(FLUTTER_BUILD_NAME)</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>$(FLUTTER_BUILD_NUMBER)</string>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>UIApplicationSceneManifest</key>
<dict>
<key>UIApplicationSupportsMultipleScenes</key>
<false/>
<key>UISceneConfigurations</key>
<dict>
<key>UIWindowSceneSessionRoleApplication</key>
<array>
<dict>
<key>UISceneClassName</key>
<string>UIWindowScene</string>
<key>UISceneConfigurationName</key>
<string>flutter</string>
<key>UISceneDelegateClassName</key>
<string>$(PRODUCT_MODULE_NAME).SceneDelegate</string>
<key>UISceneStoryboardFile</key>
<string>Main</string>
</dict>
</array>
</dict>
</dict>
<key>UIApplicationSupportsIndirectInputEvents</key>
<true/>
<key>UILaunchStoryboardName</key>
<string>LaunchScreen</string>
<key>UIMainStoryboardFile</key>
<string>Main</string>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UISupportedInterfaceOrientations~ipad</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
</dict>
</plist>

View File

@@ -0,0 +1 @@
#import "GeneratedPluginRegistrant.h"

View File

@@ -0,0 +1,6 @@
import Flutter
import UIKit
class SceneDelegate: FlutterSceneDelegate {
}

View File

@@ -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.
}
}

403
app/lib/engine.dart Normal file
View File

@@ -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<String> pieceTypes = ['I', 'O', 'T', 'S', 'Z', 'J', 'L'];
/// 피스 색 (명세서 §13 v2 확정). 'G' = 가비지.
const Map<String, String> 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<List<int>> cells;
const _Spawn(this.box, this.cells);
}
const Map<String, _Spawn> _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<String, List<int>> _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<List<int>> _rotateCW(List<List<int>> cells, int b) =>
cells.map((c) => [c[1], (b - 1) - c[0]]).toList();
List<List<List<int>>> _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<String, List<List<List<int>>>> states = {
for (final t in pieceTypes) t: _buildStates(t),
};
// ── 킥테이블 (명세서 §4.1) ──
const Map<String, List<List<int>>> _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<String, List<List<int>>> _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<List<int>> _origin = [[0, 0]];
List<List<int>> 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<String> _q = [];
SevenBag([double Function()? rng]) : rng = rng ?? (() => 0.0) {
_refill();
_refill();
}
void _refill() {
final bag = List<String>.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<String> peek([int n = Config.nextCount]) {
while (_q.length < n) {
_refill();
}
return _q.sublist(0, n);
}
}
// ── 보드 유틸 ──
List<List<String?>> createBoard([int w = Config.boardW, int h = Config.boardH]) =>
List.generate(h, (_) => List<String?>.filled(w, null));
List<List<int>> 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<List<String?>> board, List<List<int>> 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<List<String?>> board) {
final w = board[0].length, h = board.length;
final rows = <int>[];
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 = <List<String?>>[
for (int y = 0; y < h; y++)
if (!rowSet.contains(y)) board[y]
];
while (kept.length < h) {
kept.add(List<String?>.filled(w, null));
}
for (int y = 0; y < h; y++) {
board[y] = kept[y];
}
return rows.length;
}
bool isBoardEmpty(List<List<String?>> board) =>
board.every((row) => row.every((c) => c == null));
/// T-spin 판정 (명세서 §4.2). 'none' | 'mini' | 'tspin'.
String detectTSpin(List<List<String?>> 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<bool> 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<List<String?>> 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<List<int>> 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<String> 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<List<int>> 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;
}
}

34
app/lib/main.dart Normal file
View File

@@ -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,
),
),
),
),
);
}
}

213
app/pubspec.lock Normal file
View File

@@ -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"

89
app/pubspec.yaml Normal file
View File

@@ -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

204
app/test/engine_test.dart Normal file
View File

@@ -0,0 +1,204 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:tetrig/engine.dart';
Set<String> _cellSet(List<List<int>> cells) =>
cells.map((c) => '${c[0]},${c[1]}').toSet();
String _kickStr(List<List<int>> 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<String>.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);
});
}