Created
July 16, 2025 16:43
-
-
Save naranyala/079a7f5e5d7d71b227aae1f2ab995c59 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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