Files
Githug-Android/app/src/main/java/com/kawomi/githugandroid/GameModels.kt

174 lines
7.6 KiB
Kotlin

package com.kawomi.githugandroid
import androidx.compose.runtime.saveable.listSaver
enum class PlayMode(val label: String) {
CLI_ONLY("CLI ONLY"),
VISUAL("VISUAL"),
}
data class GitFile(
val name: String,
val content: String = "",
val staged: Boolean = false,
)
data class CommitNode(
val id: String,
val message: String,
)
data class RepoState(
val initialized: Boolean = false,
val files: List<GitFile> = emptyList(),
val commits: List<CommitNode> = emptyList(),
val headBranch: String = "master",
val branches: Map<String, Int> = emptyMap(),
)
data class Level(
val id: String,
val title: String,
val description: String,
val hints: List<String>,
val commandSuggestions: List<String>,
val validator: (RepoState) -> Boolean,
val setup: () -> RepoState,
)
fun sampleLevels(): List<Level> = listOf(
Level(
id = "init",
title = "Init",
description = "A new directory, git_hug, has been created. Initialize an empty repository in it.",
hints = listOf("Use git init to create a new repository.", "Try `git init` in the command area."),
commandSuggestions = listOf("git init", "git status"),
validator = { it.initialized },
setup = { RepoState() },
),
Level(
id = "add",
title = "Add",
description = "There is a file in your folder called README; add it to your staging area.",
hints = listOf("You want to stage README.", "Use `git add README`."),
commandSuggestions = listOf("git status", "git add README", "ls"),
validator = { repo -> repo.files.any { it.name == "README" && it.staged } },
setup = { RepoState(initialized = true, files = listOf(GitFile("README")), branches = mapOf("master" to 0)) },
),
Level(
id = "commit",
title = "Commit",
description = "The README file has been added to your staging area, now commit it.",
hints = listOf("You must include a message when you commit.", "Use `git commit -m \"message\"`."),
commandSuggestions = listOf("git status", "git commit -m \"Initial commit\"", "git log"),
validator = { repo -> repo.commits.isNotEmpty() },
setup = { RepoState(initialized = true, files = listOf(GitFile("README", staged = true)), branches = mapOf("master" to 0)) },
),
)
val RepoStateSaver = listSaver<RepoState, Any>(
save = { state ->
listOf(
state.initialized,
state.headBranch,
state.files.flatMap { listOf(it.name, it.content, it.staged.toString()) },
state.commits.flatMap { listOf(it.id, it.message) },
state.branches.flatMap { listOf(it.key, it.value.toString()) },
)
},
restore = { saved ->
val initialized = saved[0] as Boolean
val headBranch = saved[1] as String
val fileParts = saved[2] as List<*>
val commitParts = saved[3] as List<*>
val branchParts = saved[4] as List<*>
RepoState(
initialized = initialized,
headBranch = headBranch,
files = fileParts.chunked(3).map {
GitFile(it[0] as String, it[1] as String, (it[2] as String).toBoolean())
},
commits = commitParts.chunked(2).map {
CommitNode(it[0] as String, it[1] as String)
},
branches = branchParts.chunked(2).associate { (it[0] as String) to (it[1] as String).toInt() }
)
}
)
object GitSandboxEngine {
fun execute(repo: RepoState, command: String): Pair<RepoState, List<String>> {
val parts = command.split(" ").filter { it.isNotBlank() }
if (parts.isEmpty()) return repo to emptyList()
return when {
parts[0] == "touch" && parts.size >= 2 -> {
val name = parts[1]
if (repo.files.any { it.name == name }) repo to listOf("$name already exists")
else repo.copy(files = repo.files + GitFile(name = name)) to emptyList()
}
parts[0] == "ls" -> repo to repo.files.map { it.name }.ifEmpty { listOf() }
parts[0] != "git" -> repo to listOf("Command not supported in sandbox. Try a git command or 'touch'.")
parts.size >= 2 && parts[1] == "init" -> repo.copy(initialized = true, branches = mapOf("master" to repo.commits.size)) to listOf("Initialized empty Git repository")
!repo.initialized -> repo to listOf("fatal: not a git repository")
parts.size >= 2 && parts[1] == "status" -> repo to statusLines(repo)
parts.size >= 3 && parts[1] == "add" -> {
val target = parts[2]
val updated = repo.files.map { if (target == "." || it.name == target) it.copy(staged = true) else it }
repo.copy(files = updated) to listOf("staged ${if (target == ".") "all files" else target}")
}
parts.size >= 2 && parts[1] == "commit" -> commit(repo, parts)
parts.size >= 2 && parts[1] == "log" -> {
repo to if (repo.commits.isEmpty()) listOf("fatal: your current branch '${repo.headBranch}' does not have any commits yet")
else repo.commits.reversed().flatMap { listOf("commit ${it.id}", " ${it.message}") }
}
parts.size >= 3 && parts[1] == "branch" -> {
val branch = parts[2]
if (repo.branches.containsKey(branch)) repo to listOf("fatal: a branch named '$branch' already exists")
else repo.copy(branches = repo.branches + (branch to repo.commits.size)) to listOf("Created branch $branch")
}
parts.size >= 3 && parts[1] == "checkout" -> {
val branch = parts[2]
if (!repo.branches.containsKey(branch)) repo to listOf("error: pathspec '$branch' did not match any branch")
else repo.copy(headBranch = branch) to listOf("Switched to branch '$branch'")
}
else -> repo to listOf("Unsupported git command in MVP sandbox: ${parts.drop(1).joinToString(" ")}")
}
}
private fun commit(repo: RepoState, parts: List<String>): Pair<RepoState, List<String>> {
val messageIndex = parts.indexOf("-m")
if (messageIndex == -1 || messageIndex == parts.lastIndex) {
return repo to listOf("error: commit message required. Use git commit -m \"message\"")
}
val message = parts.drop(messageIndex + 1).joinToString(" ").trim('"')
val staged = repo.files.filter { it.staged }
if (staged.isEmpty()) return repo to listOf("nothing to commit")
val nextId = "${repo.commits.size + 1}".padStart(7, '0')
val cleanedFiles = repo.files.map { it.copy(staged = false) }
return repo.copy(
files = cleanedFiles,
commits = repo.commits + CommitNode(nextId, message),
branches = repo.branches + (repo.headBranch to (repo.commits.size + 1)),
) to listOf("[$nextId] $message")
}
private fun statusLines(repo: RepoState): List<String> {
val staged = repo.files.filter { it.staged }.map { "new file: ${it.name}" }
val unstaged = repo.files.filterNot { it.staged }.map { "untracked: ${it.name}" }
return buildList {
add("On branch ${repo.headBranch}")
if (staged.isEmpty() && unstaged.isEmpty()) {
add("nothing to commit, working tree clean")
} else {
if (staged.isNotEmpty()) {
add("Changes to be committed:")
addAll(staged)
}
if (unstaged.isNotEmpty()) {
add("Untracked files:")
addAll(unstaged)
}
}
}
}
}