Skip to content

Instantly share code, notes, and snippets.

@naranyala
Created July 16, 2025 16:43
Show Gist options
  • Select an option

  • Save naranyala/079a7f5e5d7d71b227aae1f2ab995c59 to your computer and use it in GitHub Desktop.

Select an option

Save naranyala/079a7f5e5d7d71b227aae1f2ab995c59 to your computer and use it in GitHub Desktop.
import kotlinx.cinterop.*
import raylib.*
import kotlin.random.Random
@OptIn(ExperimentalForeignApi::class)
object Colors {
val BLACK = cValue<Color> { r = 0u; g = 0u; b = 0u; a = 255u }
val WHITE = cValue<Color> { r = 255u; g = 255u; b = 255u; a = 255u }
val GRAY = cValue<Color> { r = 187u; g = 173u; b = 160u; a = 255u }
val DARK_GRAY = cValue<Color> { r = 119u; g = 110u; b = 101u; a = 255u }
val LIGHT_GRAY = cValue<Color> { r = 205u; g = 193u; b = 180u; a = 255u }
val TILE_2 = cValue<Color> { r = 238u; g = 228u; b = 218u; a = 255u }
val TILE_4 = cValue<Color> { r = 237u; g = 224u; b = 200u; a = 255u }
val TILE_8 = cValue<Color> { r = 242u; g = 177u; b = 121u; a = 255u }
val TILE_16 = cValue<Color> { r = 245u; g = 149u; b = 99u; a = 255u }
val TILE_32 = cValue<Color> { r = 246u; g = 124u; b = 95u; a = 255u }
val TILE_64 = cValue<Color> { r = 246u; g = 94u; b = 59u; a = 255u }
val TILE_128 = cValue<Color> { r = 237u; g = 207u; b = 114u; a = 255u }
val TILE_256 = cValue<Color> { r = 237u; g = 204u; b = 97u; a = 255u }
val TILE_512 = cValue<Color> { r = 237u; g = 200u; b = 80u; a = 255u }
val TILE_1024 = cValue<Color> { r = 237u; g = 197u; b = 63u; a = 255u }
val TILE_2048 = cValue<Color> { r = 237u; g = 194u; b = 46u; a = 255u }
val TILE_DEFAULT_TEXT = cValue<Color> { r = 119u; g = 110u; b = 101u; a = 255u }
val TILE_DARK_TEXT = cValue<Color> { r = 249u; g = 246u; b = 242u; a = 255u }
val GAME_OVER_OVERLAY = cValue<Color> { r = 255u; g = 255u; b = 255u; a = 150u }
val GAME_OVER_TEXT = cValue<Color> { r = 119u; g = 110u; b = 101u; a = 255u }
}
@OptIn(ExperimentalForeignApi::class)
class Game2048 {
private val screenWidth = 600
private val screenHeight = 700
private val gridSize = 4
private val tileSize = 120f
private val tileSpacing = 15f
private val boardOffset: CValue<Vector2> = cValue<Vector2> {
x = (screenWidth - (gridSize * tileSize + (gridSize + 1) * tileSpacing)) / 2f
y = (screenHeight - (gridSize * tileSize + (gridSize + 1) * tileSpacing)) / 2f + 50f
}
private var board: Array<Array<Int>> = Array(gridSize) { Array(gridSize) { 0 } }
private var score = 0
private var gameOver = false
private var gameWon = false
init {
initGame()
}
private fun initGame() {
board = Array(gridSize) { Array(gridSize) { 0 } }
score = 0
gameOver = false
gameWon = false
addNewTile()
addNewTile()
}
private fun addNewTile() {
val emptyCells = mutableListOf<Pair<Int, Int>>()
for (row in 0 until gridSize) {
for (col in 0 until gridSize) {
if (board[row][col] == 0) {
emptyCells.add(Pair(row, col))
}
}
}
if (emptyCells.isNotEmpty()) {
val (row, col) = emptyCells[Random.nextInt(emptyCells.size)]
board[row][col] = if (Random.nextFloat() < 0.9f) 2 else 4
}
}
private fun checkGameOver() {
if (hasEmptyCells()) return
for (row in 0 until gridSize) {
for (col in 0 until gridSize) {
val current = board[row][col]
if (current == 0) continue
if (col < gridSize - 1 && board[row][col + 1] == current) return
if (row < gridSize - 1 && board[row + 1][col] == current) return
}
}
gameOver = true
}
private fun hasEmptyCells(): Boolean {
return board.any { row -> row.any { it == 0 } }
}
private fun move(direction: String): Boolean {
if (gameOver || gameWon) return false
var moved = false
val originalBoard = board.map { it.copyOf() }.toTypedArray()
when (direction) {
"UP" -> {
for (col in 0 until gridSize) {
val column = (0 until gridSize).map { board[it][col] }.toMutableList()
val newColumn = slideAndMerge(column)
for (row in 0 until gridSize) {
board[row][col] = newColumn[row]
}
}
}
"DOWN" -> {
for (col in 0 until gridSize) {
val column = (0 until gridSize).map { board[it][col] }.reversed().toMutableList()
val newColumn = slideAndMerge(column).reversed()
for (row in 0 until gridSize) {
board[row][col] = newColumn[row]
}
}
}
"LEFT" -> {
for (row in 0 until gridSize) {
board[row] = slideAndMerge(board[row].toMutableList()).toTypedArray()
}
}
"RIGHT" -> {
for (row in 0 until gridSize) {
board[row] = slideAndMerge(board[row].reversed().toMutableList()).reversed().toTypedArray()
}
}
}
moved = board.indices.any { row ->
board[row].indices.any { col ->
originalBoard[row][col] != board[row][col]
}
}
if (moved) {
addNewTile()
checkGameOver()
}
return moved
}
private fun slideAndMerge(list: MutableList<Int>): MutableList<Int> {
val result = MutableList(gridSize) { 0 }
var pos = 0
var i = 0
while (i < list.size && pos < gridSize) {
if (list[i] == 0) {
i++
continue
}
if (i + 1 < list.size && list[i] == list[i + 1]) {
val merged = list[i] * 2
result[pos] = merged
score += merged
if (merged == 2048) gameWon = true
i += 2
} else {
result[pos] = list[i]
i++
}
pos++
}
return result
}
private fun handleInput() {
if (IsKeyPressed(265)) move("UP") // KEY_UP
if (IsKeyPressed(264)) move("DOWN") // KEY_DOWN
if (IsKeyPressed(263)) move("LEFT") // KEY_LEFT
if (IsKeyPressed(262)) move("RIGHT") // KEY_RIGHT
if (gameOver || gameWon) {
if (IsKeyPressed(82)) initGame() // KEY_R
}
}
private fun getTileColor(value: Int): CValue<Color> = when (value) {
2 -> Colors.TILE_2
4 -> Colors.TILE_4
8 -> Colors.TILE_8
16 -> Colors.TILE_16
32 -> Colors.TILE_32
64 -> Colors.TILE_64
128 -> Colors.TILE_128
256 -> Colors.TILE_256
512 -> Colors.TILE_512
1024 -> Colors.TILE_1024
2048 -> Colors.TILE_2048
else -> Colors.LIGHT_GRAY
}
private fun getTileTextColor(value: Int): CValue<Color> =
if (value == 2 || value == 4) Colors.TILE_DEFAULT_TEXT else Colors.TILE_DARK_TEXT
private fun draw() {
BeginDrawing()
ClearBackground(Colors.WHITE)
val scoreText = "SCORE: $score"
DrawText(scoreText, screenWidth / 2 - MeasureText(scoreText, 40) / 2, 20, 40, Colors.DARK_GRAY)
val boardWidth = gridSize * tileSize + (gridSize + 1) * tileSpacing
val boardHeight = gridSize * tileSize + (gridSize + 1) * tileSpacing
boardOffset.useContents {
DrawRectangle(x.toInt(), y.toInt(), boardWidth.toInt(), boardHeight.toInt(), Colors.GRAY)
for (row in 0 until gridSize) {
for (col in 0 until gridSize) {
val tileX = x + col * (tileSize + tileSpacing) + tileSpacing
val tileY = y + row * (tileSize + tileSpacing) + tileSpacing
val tileValue = board[row][col]
DrawRectangle(tileX.toInt(), tileY.toInt(), tileSize.toInt(), tileSize.toInt(), getTileColor(tileValue))
if (tileValue != 0) {
val text = tileValue.toString()
val fontSize = when {
tileValue >= 1000 -> 30
tileValue >= 100 -> 35
else -> 40
}
val textWidth = MeasureText(text, fontSize)
val textX = tileX + (tileSize - textWidth) / 2
val textY = tileY + (tileSize - fontSize) / 2
DrawText(text, textX.toInt(), textY.toInt(), fontSize, getTileTextColor(tileValue))
}
}
}
}
if (gameOver || gameWon) {
DrawRectangle(0, 0, screenWidth, screenHeight, Colors.GAME_OVER_OVERLAY)
val message = if (gameWon) "YOU WIN!" else "GAME OVER!"
val restartMessage = "Press 'R' to restart"
val messageFontSize = 70
val restartFontSize = 30
DrawText(
message,
screenWidth / 2 - MeasureText(message, messageFontSize) / 2,
screenHeight / 2 - messageFontSize / 2 - 40,
messageFontSize,
Colors.GAME_OVER_TEXT
)
DrawText(
restartMessage,
screenWidth / 2 - MeasureText(restartMessage, restartFontSize) / 2,
screenHeight / 2 + restartFontSize / 2 + 20,
restartFontSize,
Colors.GAME_OVER_TEXT
)
}
EndDrawing()
}
fun run() {
InitWindow(screenWidth, screenHeight, "2048 Game - Kotlin/Native Raylib")
SetTargetFPS(60)
while (!WindowShouldClose()) {
handleInput()
draw()
}
CloseWindow()
}
}
@OptIn(ExperimentalForeignApi::class)
fun main() {
val game = Game2048()
game.run()
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment