2 Commits
Author SHA1 Message Date
DawnsorrowandCursor 8b6cc0bedc Update Gitea publish script for v0.2.0 releases.
Deploy / Build Kotlin/Wasm (push) Canceled after 0s
Build Android APK / Android (push) Canceled after 0s
Build Desktop app / Build Desktop app (push) Canceled after 0s
Co-authored-by: Cursor <[email protected]>
2026-07-29 08:15:46 -05:00
DawnsorrowandCursor a9846fd832 Add hexagon board layout and fix settings icon sizing.
Rows vs Hexagon selector, side-length sizing for hex boards, and
constrain settings icons so sliders stay usable.

Co-authored-by: Cursor <[email protected]>
2026-07-29 08:14:58 -05:00
11 changed files with 420 additions and 113 deletions
@@ -43,6 +43,7 @@ import androidx.compose.ui.platform.LocalUriHandler
import androidx.compose.ui.unit.dp
import com.russhwolf.settings.get
import com.russhwolf.settings.set
import de.stefan_oltmann.mines.model.BoardLayout
import de.stefan_oltmann.mines.model.Game
import de.stefan_oltmann.mines.model.GameConfig
import de.stefan_oltmann.mines.model.GameDifficulty
@@ -88,8 +89,12 @@ fun App() {
mapHeight = settings["mines_map_height"] ?: defaultMapHeight,
mineCount = resolveSavedMineCount(
mapWidth = settings["mines_map_width"] ?: defaultMapWidth,
mapHeight = settings["mines_map_height"] ?: defaultMapHeight
)
mapHeight = settings["mines_map_height"] ?: defaultMapHeight,
boardLayout = savedBoardLayout(),
hexSideLength = settings["mines_hex_side"] ?: DEFAULT_HEX_SIDE
),
boardLayout = savedBoardLayout(),
hexSideLength = settings["mines_hex_side"] ?: DEFAULT_HEX_SIDE
)
)
}
@@ -138,18 +143,29 @@ fun App() {
val oldMapWidth = settings["mines_map_width"] ?: defaultMapWidth
val oldMapHeight = settings["mines_map_height"] ?: defaultMapHeight
val oldMineCount = resolveSavedMineCount(oldMapWidth, oldMapHeight)
val oldBoardLayout = savedBoardLayout()
val oldHexSideLength = settings["mines_hex_side"] ?: DEFAULT_HEX_SIDE
val oldMineCount = resolveSavedMineCount(
oldMapWidth,
oldMapHeight,
oldBoardLayout,
oldHexSideLength
)
/* Save new settings to config */
settings["mines_cell_size"] = newGameConfig.cellSize
settings["mines_map_width"] = newGameConfig.mapWidth
settings["mines_map_height"] = newGameConfig.mapHeight
settings["mines_mine_count"] = newGameConfig.mineCount
settings["mines_board_layout"] = newGameConfig.boardLayout.toSettingsValue()
settings["mines_hex_side"] = newGameConfig.hexSideLength
val mapSettingsChanged =
oldMapWidth != newGameConfig.mapWidth ||
oldMapHeight != newGameConfig.mapHeight ||
oldMineCount != newGameConfig.mineCount
oldMineCount != newGameConfig.mineCount ||
oldBoardLayout != newGameConfig.boardLayout ||
oldHexSideLength != newGameConfig.hexSideLength
/* Launch a new game every time the settings change something that influences the map */
if (mapSettingsChanged) {
@@ -346,18 +362,34 @@ fun App() {
* Prefer an explicit saved mine count. Fall back to the old difficulty
* preset so existing installs keep a sensible default.
*/
private fun resolveSavedMineCount(mapWidth: Int, mapHeight: Int): Int {
private fun resolveSavedMineCount(
mapWidth: Int,
mapHeight: Int,
boardLayout: BoardLayout,
hexSideLength: Int
): Int {
val savedMineCount: Int? = settings["mines_mine_count"]
if (savedMineCount != null)
return clampMineCount(savedMineCount, mapWidth, mapHeight)
return clampMineCount(
savedMineCount,
mapWidth,
mapHeight,
boardLayout,
hexSideLength
)
val difficulty = GameDifficulty.fromSettingsValue(settings["mines_difficulty"])
return clampMineCount(
mineCount = difficulty.calcMineCount(mapWidth, mapHeight),
mapWidth = mapWidth,
mapHeight = mapHeight
mapHeight = mapHeight,
boardLayout = boardLayout,
hexSideLength = hexSideLength
)
}
private fun savedBoardLayout(): BoardLayout =
BoardLayout.fromSettingsValue(settings["mines_board_layout"])
@@ -28,6 +28,10 @@ const val DEFAULT_CELL_SIZE: Int = 40
const val MIN_LONG_SIDE: Int = 5
const val MAX_LONG_SIDE: Int = 50
const val MIN_HEX_SIDE: Int = 3
const val MAX_HEX_SIDE: Int = 29
const val DEFAULT_HEX_SIDE: Int = 8
const val MIN_MINE_COUNT: Int = 1
const val FONT_SIZE: Int = 20
@@ -0,0 +1,26 @@
/*
* 💣 Mines 💣
* Copyright (C) 2025 Stefan Oltmann
* https://github.com/StefanOltmann/mines
*/
package de.stefan_oltmann.mines.model
enum class BoardLayout(
private val settingsValue: String
) {
ROWS("rows"),
HEXAGON("hexagon");
fun toSettingsValue(): String = settingsValue
companion object {
fun fromSettingsValue(value: String?): BoardLayout =
when (value?.trim()?.lowercase()) {
HEXAGON.settingsValue -> HEXAGON
else -> ROWS
}
}
}
@@ -19,38 +19,142 @@
package de.stefan_oltmann.mines.model
import de.stefan_oltmann.mines.DEFAULT_HEX_SIDE
import de.stefan_oltmann.mines.MIN_HEX_SIDE
import de.stefan_oltmann.mines.MIN_LONG_SIDE
import de.stefan_oltmann.mines.MIN_MINE_COUNT
import kotlin.math.abs
data class GameConfig(
val cellSize: Int,
val mapWidth: Int,
val mapHeight: Int,
val mineCount: Int
val mineCount: Int,
val boardLayout: BoardLayout = BoardLayout.ROWS,
val hexSideLength: Int = DEFAULT_HEX_SIDE
) {
init {
/* Ensure no illegal configs can be created. */
require(mapWidth >= MIN_LONG_SIDE) { "Map width must be greater than $MIN_LONG_SIDE." }
require(mapHeight >= MIN_LONG_SIDE) { "Map height must be greater than $MIN_LONG_SIDE." }
require(hexSideLength >= MIN_HEX_SIDE) { "Hex side must be at least $MIN_HEX_SIDE." }
require(mineCount >= MIN_MINE_COUNT) { "Mine count must be at least $MIN_MINE_COUNT." }
require(mineCount <= maxPlaceableMines(mapWidth, mapHeight)) {
require(
mineCount <= maxPlaceableMines(
mapWidth = mapWidth,
mapHeight = mapHeight,
boardLayout = boardLayout,
hexSideLength = hexSideLength
)
) {
"Mine count must fit outside the protected starting area."
}
}
val boardWidth: Int
get() = when (boardLayout) {
BoardLayout.ROWS -> mapWidth
BoardLayout.HEXAGON -> hexSideLength * 2 - 1
}
val boardHeight: Int
get() = when (boardLayout) {
BoardLayout.ROWS -> mapHeight
BoardLayout.HEXAGON -> hexSideLength * 2 - 1
}
fun isCellActive(x: Int, y: Int): Boolean =
when (boardLayout) {
BoardLayout.ROWS ->
x in 0 until mapWidth && y in 0 until mapHeight
BoardLayout.HEXAGON ->
hexDistanceFromCenter(x, y, hexSideLength) < hexSideLength
}
fun isCellProtected(x: Int, y: Int): Boolean =
when (boardLayout) {
BoardLayout.ROWS ->
x in calcProtectedRange(mapWidth) &&
y in calcProtectedRange(mapHeight)
BoardLayout.HEXAGON -> {
val protectedSide = (hexSideLength * 0.3f).toInt().coerceAtLeast(2)
hexDistanceFromCenter(x, y, hexSideLength) < protectedSide
}
}
}
/**
* Maximum mines that can be placed without filling the protected center zone.
*/
fun maxPlaceableMines(mapWidth: Int, mapHeight: Int): Int {
fun maxPlaceableMines(
mapWidth: Int,
mapHeight: Int,
boardLayout: BoardLayout = BoardLayout.ROWS,
hexSideLength: Int = DEFAULT_HEX_SIDE
): Int {
val protectedCells =
Minefield.calcProtectedRange(mapWidth).count() *
Minefield.calcProtectedRange(mapHeight).count()
val activeCells = when (boardLayout) {
BoardLayout.ROWS -> mapWidth * mapHeight
BoardLayout.HEXAGON -> 3 * hexSideLength * (hexSideLength - 1) + 1
}
return (mapWidth * mapHeight - protectedCells).coerceAtLeast(MIN_MINE_COUNT)
val protectedCells = when (boardLayout) {
BoardLayout.ROWS ->
calcProtectedRange(mapWidth).count() *
calcProtectedRange(mapHeight).count()
BoardLayout.HEXAGON -> {
val protectedSide = (hexSideLength * 0.3f).toInt().coerceAtLeast(2)
3 * protectedSide * (protectedSide - 1) + 1
}
}
return (activeCells - protectedCells).coerceAtLeast(MIN_MINE_COUNT)
}
fun clampMineCount(mineCount: Int, mapWidth: Int, mapHeight: Int): Int =
mineCount.coerceIn(MIN_MINE_COUNT, maxPlaceableMines(mapWidth, mapHeight))
fun clampMineCount(
mineCount: Int,
mapWidth: Int,
mapHeight: Int,
boardLayout: BoardLayout = BoardLayout.ROWS,
hexSideLength: Int = DEFAULT_HEX_SIDE
): Int =
mineCount.coerceIn(
MIN_MINE_COUNT,
maxPlaceableMines(mapWidth, mapHeight, boardLayout, hexSideLength)
)
/* Calculates a centered protected range that scales with board size. */
fun calcProtectedRange(length: Int): IntRange {
val targetSize = (length * 0.3).toInt().coerceAtLeast(2)
val protectedSize =
if (targetSize % 2 == length % 2)
targetSize
else
targetSize + 1
val start = (length - protectedSize) / 2
return start until (start + protectedSize)
}
/**
* Cube distance from a cell to the center of an odd-q offset hex board.
*/
private fun hexDistanceFromCenter(x: Int, y: Int, sideLength: Int): Int {
val centerX = sideLength - 1
val centerY = sideLength - 1
val axialR = y - (x - (x and 1)) / 2
val centerAxialR = centerY - (centerX - (centerX and 1)) / 2
val dq = x - centerX
val dr = axialR - centerAxialR
val ds = -dq - dr
return maxOf(abs(dq), abs(dr), abs(ds))
}
@@ -53,7 +53,11 @@ class GameState(
for (x in 0 until minefield.width)
for (y in 0 until minefield.height)
if (!minefield.isMine(x, y) && !isRevealed(x, y))
if (
minefield.isActive(x, y) &&
!minefield.isMine(x, y) &&
!isRevealed(x, y)
)
return false
return true
@@ -61,6 +65,9 @@ class GameState(
fun reveal(x: Int, y: Int) {
if (!minefield.isActive(x, y))
return
/* Ignore call if coordinates are already revealed. */
if (revealedMatrix[x][y])
return
@@ -131,6 +138,7 @@ class GameState(
flaggedMatrix[x][y]
fun toggleFlag(x: Int, y: Int) {
if (minefield.isActive(x, y))
flaggedMatrix[x][y] = !flaggedMatrix[x][y]
}
@@ -164,8 +172,10 @@ class GameState(
x = x,
y = y,
width = minefield.width,
height = minefield.height,
action = action
)
height = minefield.height
) { adjX, adjY ->
if (minefield.isActive(adjX, adjY))
action(adjX, adjY)
}
}
}
@@ -28,16 +28,19 @@ class Minefield(
) {
val width
get() = config.mapWidth
get() = config.boardWidth
val height
get() = config.mapHeight
get() = config.boardHeight
fun getCellType(x: Int, y: Int): CellType =
matrix[x][y]
fun isActive(x: Int, y: Int): Boolean =
config.isCellActive(x, y)
fun isMine(x: Int, y: Int): Boolean =
matrix[x][y] == CellType.MINE
isActive(x, y) && matrix[x][y] == CellType.MINE
companion object {
@@ -49,25 +52,21 @@ class Minefield(
config = config,
seed = seed,
matrix = createMatrix(
width = config.mapWidth,
height = config.mapHeight,
mineCount = config.mineCount,
config = config,
seed = seed
)
)
private fun createMatrix(
width: Int,
height: Int,
mineCount: Int,
config: GameConfig,
seed: Int
): Array<Array<CellType>> {
val matrix = createEmptyMatrix(width, height)
val matrix = createEmptyMatrix(config.boardWidth, config.boardHeight)
placeMines(matrix, width, height, mineCount, seed)
placeMines(matrix, config, seed)
placeCounts(matrix, width, height)
placeCounts(matrix, config)
return matrix
}
@@ -79,77 +78,39 @@ class Minefield(
}
}
/* Calculates a centered protected range that scales with board size */
fun calcProtectedRange(length: Int): IntRange {
val targetSize = (length * 0.3).toInt().coerceAtLeast(2)
val protectedSize =
if (targetSize % 2 == length % 2)
targetSize
else
targetSize + 1
val start = (length - protectedSize) / 2
return start until (start + protectedSize)
}
private fun placeMines(
matrix: Array<Array<CellType>>,
width: Int,
height: Int,
mineCount: Int,
config: GameConfig,
seed: Int
) {
/*
* Mines are placed according to seed to reproduce results.
*/
val random = Random(seed)
val protectedXRange = calcProtectedRange(width)
val protectedYRange = calcProtectedRange(height)
var placedMinesCount = 0
while (placedMinesCount < mineCount) {
val x = random.nextInt(width)
val y = random.nextInt(height)
/*
* Keep the middle free of mines to give players a starting point.
*/
if (x in protectedXRange && y in protectedYRange)
continue
/*
* Only place mines into empty cells.
*
* This guarantees that we have enough mines,
* even if the randomizer selects the same cell twice.
*/
if (matrix[x][y] == CellType.EMPTY) {
matrix[x][y] = CellType.MINE
placedMinesCount++
val candidates = buildList {
for (x in 0 until config.boardWidth) {
for (y in 0 until config.boardHeight) {
if (config.isCellActive(x, y) && !config.isCellProtected(x, y))
add(x to y)
}
}
}
candidates
.shuffled(Random(seed))
.take(config.mineCount)
.forEach { (x, y) ->
matrix[x][y] = CellType.MINE
}
}
private fun placeCounts(
matrix: Array<Array<CellType>>,
width: Int,
height: Int
config: GameConfig
) {
for (x in 0 until width) {
for (y in 0 until height) {
for (x in 0 until config.boardWidth) {
for (y in 0 until config.boardHeight) {
/* Minefields stay as they are. */
if (matrix[x][y] == CellType.MINE)
/* Inactive cells and minefields stay as they are. */
if (!config.isCellActive(x, y) || matrix[x][y] == CellType.MINE)
continue
val mineCount = countMinesInAdjacentCells(matrix, x, y)
@@ -111,6 +111,9 @@ fun MinefieldCanvas(
if (row !in 0 until gameState.minefield.height)
return null
if (!gameState.minefield.isActive(col, row))
return null
return IntOffset(col, row)
}
@@ -168,6 +171,9 @@ fun MinefieldCanvas(
for (x in 0 until gameState.minefield.width) {
for (y in 0 until gameState.minefield.height) {
if (!gameState.minefield.isActive(x, y))
continue
val (cxRel, cyRel) = HexGeometry.cellCenter(x, y, hexSize)
val center = Offset(originX + cxRel, originY + cyRel)
@@ -21,6 +21,7 @@ package de.stefan_oltmann.mines.ui
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
@@ -28,6 +29,7 @@ import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.widthIn
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
@@ -47,10 +49,13 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import de.stefan_oltmann.mines.FONT_SIZE
import de.stefan_oltmann.mines.MAX_CELL_SIZE
import de.stefan_oltmann.mines.MAX_HEX_SIDE
import de.stefan_oltmann.mines.MAX_LONG_SIDE
import de.stefan_oltmann.mines.MIN_CELL_SIZE
import de.stefan_oltmann.mines.MIN_HEX_SIDE
import de.stefan_oltmann.mines.MIN_LONG_SIDE
import de.stefan_oltmann.mines.MIN_MINE_COUNT
import de.stefan_oltmann.mines.model.BoardLayout
import de.stefan_oltmann.mines.model.GameConfig
import de.stefan_oltmann.mines.model.clampMineCount
import de.stefan_oltmann.mines.model.maxPlaceableMines
@@ -83,16 +88,28 @@ fun SettingsDialog(
val mapWidth = remember { mutableStateOf(gameConfig.mapWidth.toFloat()) }
val mapHeight = remember { mutableStateOf(gameConfig.mapHeight.toFloat()) }
val mineCount = remember { mutableStateOf(gameConfig.mineCount.toFloat()) }
val boardLayout = remember { mutableStateOf(gameConfig.boardLayout) }
val hexSideLength = remember { mutableStateOf(gameConfig.hexSideLength.toFloat()) }
fun clampMinesToBoard() {
mineCount.value = clampMineCount(
mineCount = mineCount.value.toInt(),
mapWidth = mapWidth.value.toInt(),
mapHeight = mapHeight.value.toInt()
mapHeight = mapHeight.value.toInt(),
boardLayout = boardLayout.value,
hexSideLength = hexSideLength.value.toInt()
).toFloat()
}
fun currentMaxMines(): Int =
maxPlaceableMines(
mapWidth = mapWidth.value.toInt(),
mapHeight = mapHeight.value.toInt(),
boardLayout = boardLayout.value,
hexSideLength = hexSideLength.value.toInt()
)
Box(
contentAlignment = Alignment.Center,
modifier = Modifier
@@ -129,7 +146,8 @@ fun SettingsDialog(
Icon(
imageVector = IconZoom,
contentDescription = null,
tint = colorForeground
tint = colorForeground,
modifier = Modifier.size(24.dp)
)
Slider(
@@ -152,6 +170,47 @@ fun SettingsDialog(
)
}
Row(
horizontalArrangement = Arrangement.spacedBy(defaultSpacing)
) {
for (layout in BoardLayout.entries) {
Box(
contentAlignment = Alignment.Center,
modifier = Modifier
.height(buttonSize)
.weight(0.5f)
.background(colorCellHidden, defaultRoundedCornerShape)
.border(
width = 1.dp,
color = if (boardLayout.value == layout)
colorForeground
else
Color.Transparent,
shape = defaultRoundedCornerShape
)
.noRippleClickable {
boardLayout.value = layout
clampMinesToBoard()
}
) {
Text(
text = when (layout) {
BoardLayout.ROWS -> "Rows"
BoardLayout.HEXAGON -> "Hexagon"
},
fontFamily = fontFamily,
color = colorForeground,
fontSize = FONT_SIZE.sp
)
}
}
}
if (boardLayout.value == BoardLayout.ROWS) {
Row(
horizontalArrangement = Arrangement.spacedBy(defaultSpacing),
verticalAlignment = Alignment.CenterVertically,
@@ -161,7 +220,8 @@ fun SettingsDialog(
Icon(
imageVector = IconWidth,
contentDescription = null,
tint = colorForeground
tint = colorForeground,
modifier = Modifier.size(24.dp)
)
Slider(
@@ -194,7 +254,8 @@ fun SettingsDialog(
Icon(
imageVector = IconHeight,
contentDescription = null,
tint = colorForeground
tint = colorForeground,
modifier = Modifier.size(24.dp)
)
Slider(
@@ -218,6 +279,43 @@ fun SettingsDialog(
)
}
} else {
Row(
horizontalArrangement = Arrangement.spacedBy(defaultSpacing),
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.padding(horizontal = defaultSpacing)
) {
Icon(
imageVector = IconWidth,
contentDescription = null,
tint = colorForeground,
modifier = Modifier.size(24.dp)
)
Slider(
value = hexSideLength.value,
onValueChange = {
hexSideLength.value = it
clampMinesToBoard()
},
valueRange = MIN_HEX_SIDE.toFloat()..MAX_HEX_SIDE.toFloat(),
colors = sliderColors,
modifier = Modifier.weight(1F)
)
Text(
text = hexSideLength.value.toInt().toString(),
fontFamily = fontFamily,
color = colorForeground,
fontSize = FONT_SIZE.sp,
textAlign = TextAlign.Right,
modifier = Modifier.widthIn(min = 20.dp)
)
}
}
Row(
horizontalArrangement = Arrangement.spacedBy(defaultSpacing),
verticalAlignment = Alignment.CenterVertically,
@@ -227,24 +325,19 @@ fun SettingsDialog(
Icon(
imageVector = IconMines,
contentDescription = null,
tint = colorForeground
tint = colorForeground,
modifier = Modifier.size(24.dp)
)
Slider(
value = mineCount.value.coerceIn(
MIN_MINE_COUNT.toFloat(),
maxPlaceableMines(
mapWidth.value.toInt(),
mapHeight.value.toInt()
).toFloat()
currentMaxMines().toFloat()
),
onValueChange = {
mineCount.value = it
},
valueRange = MIN_MINE_COUNT.toFloat()..maxPlaceableMines(
mapWidth.value.toInt(),
mapHeight.value.toInt()
).toFloat(),
valueRange = MIN_MINE_COUNT.toFloat()..currentMaxMines().toFloat(),
colors = sliderColors,
modifier = Modifier.weight(1F)
)
@@ -305,8 +398,12 @@ fun SettingsDialog(
mineCount = clampMineCount(
mineCount = mineCount.value.toInt(),
mapWidth = width,
mapHeight = height
)
mapHeight = height,
boardLayout = boardLayout.value,
hexSideLength = hexSideLength.value.toInt()
),
boardLayout = boardLayout.value,
hexSideLength = hexSideLength.value.toInt()
)
)
}
@@ -13,8 +13,8 @@ val IconMines: ImageVector
}
_IconMines = ImageVector.Builder(
name = "IconMines",
defaultWidth = 512.dp,
defaultHeight = 512.dp,
defaultWidth = 24.dp,
defaultHeight = 24.dp,
viewportWidth = 512f,
viewportHeight = 512f
).apply {
@@ -0,0 +1,65 @@
package de.stefan_oltmann.mines.model
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertFalse
import kotlin.test.assertTrue
class BoardLayoutTest {
@Test
fun hexagonSideThreeHasNineteenActiveCells() {
val config = GameConfig(
cellSize = 40,
mapWidth = 10,
mapHeight = 10,
mineCount = 5,
boardLayout = BoardLayout.HEXAGON,
hexSideLength = 3
)
assertEquals(5, config.boardWidth)
assertEquals(5, config.boardHeight)
val activeCells = buildList {
for (x in 0 until config.boardWidth)
for (y in 0 until config.boardHeight)
if (config.isCellActive(x, y))
add(x to y)
}
assertEquals(19, activeCells.size)
assertTrue(config.isCellActive(2, 2))
assertFalse(config.isCellActive(0, 0))
assertFalse(config.isCellActive(4, 0))
}
@Test
fun minesOnlyAppearOnActiveHexagonCells() {
val config = GameConfig(
cellSize = 40,
mapWidth = 10,
mapHeight = 10,
mineCount = 10,
boardLayout = BoardLayout.HEXAGON,
hexSideLength = 5
)
val minefield = Minefield.create(config, seed = 1234)
var mines = 0
for (x in 0 until minefield.width) {
for (y in 0 until minefield.height) {
if (minefield.isMine(x, y)) {
mines++
assertTrue(minefield.isActive(x, y))
assertFalse(config.isCellProtected(x, y))
}
}
}
assertEquals(config.mineCount, mines)
}
}
+8 -6
View File
@@ -1,8 +1,9 @@
#!/usr/bin/env bash
# Create Dawnsorrow/hex-mines on Gitea (if needed), push main, create v0.1.0
# Create Dawnsorrow/hex-mines on Gitea (if needed), push main, create a
# release, and upload the debug APK.
#
# Requires: GITEA_TOKEN (repo write), curl, python3, git
# Optional: HEX_MINES_VERSION (default 0.2.0)
set -euo pipefail
ROOT="$(cd "$(dirname "$0")" && pwd)"
@@ -10,7 +11,7 @@ OWNER="${GITEA_OWNER:-Dawnsorrow}"
REPO="${GITEA_REPO:-hex-mines}"
BASE="${GITEA_BASE_URL:-https://git.hisora.dev}"
BASE="${BASE%/}"
VERSION="${HEX_MINES_VERSION:-0.1.0}"
VERSION="${HEX_MINES_VERSION:-0.2.0}"
TAG="v${VERSION}"
TITLE="Hex Mines ${TAG}"
APK="${1:-${ROOT}/dist/HexMines-${VERSION}-debug.apk}"
@@ -74,7 +75,8 @@ else
fi
echo "Pushing main and tag ${TAG} ..."
git push -u "$PUSH_URL_AUTH" HEAD:main
git push "$PUSH_URL_AUTH" HEAD:main
git branch --set-upstream-to=origin/main main 2>/dev/null || true
# Create / move tag locally
git tag -f "$TAG"
@@ -83,13 +85,13 @@ git push -f "$PUSH_URL_AUTH" "refs/tags/${TAG}"
NOTE=$(cat <<EOF
## Hex Mines ${TAG}
First public build of the hexagonal minesweeper fork.
Hexagonal minesweeper fork of [StefanOltmann/mines](https://github.com/StefanOltmann/mines) (AGPL-3.0).
### Highlights
- Flat-top hex tiles (6 neighbors)
- Configurable board width / height
- Layout modes: **Rows** (width × height) and **Hexagon** (side length)
- Configurable mine count slider
- Based on [StefanOltmann/mines](https://github.com/StefanOltmann/mines) (AGPL-3.0)
- Settings icon sizing fix for usable sliders
### Install
Download \`HexMines-${VERSION}-debug.apk\` and install on Android (debug-signed).