Skip to content

Commit

Permalink
Merge pull request #5
Browse files Browse the repository at this point in the history
* 2048 :3

* Merge branch 'refs/heads/main' into fork/ThatGravyBoat/feat/2048

* fix merge

* add constrainTo infix

* Fix logic

* Merge branch 'main' into feat/2048

* fix tile generation on move/merge fail
  • Loading branch information
ThatGravyBoat authored Sep 29, 2024
1 parent 580a551 commit acb8e6b
Show file tree
Hide file tree
Showing 6 changed files with 355 additions and 0 deletions.
166 changes: 166 additions & 0 deletions src/main/kotlin/gay/j10a1n15/sillygames/games/numbers/2048.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
package gay.j10a1n15.sillygames.games.numbers

import gay.j10a1n15.sillygames.data.KeybindSet
import gay.j10a1n15.sillygames.games.Game
import gay.j10a1n15.sillygames.games.GameInformation
import gay.j10a1n15.sillygames.games.wordle.WordlePalette
import gay.j10a1n15.sillygames.rpc.RpcInfo
import gay.j10a1n15.sillygames.rpc.RpcProvider
import gay.j10a1n15.sillygames.utils.essentials.ElementaUtils.constrainTo
import gg.essential.elementa.UIComponent
import gg.essential.elementa.components.UIBlock
import gg.essential.elementa.components.UIContainer
import gg.essential.elementa.components.UIText
import gg.essential.elementa.constraints.AspectConstraint
import gg.essential.elementa.constraints.CenterConstraint
import gg.essential.elementa.constraints.MinConstraint
import gg.essential.elementa.dsl.childOf
import gg.essential.elementa.dsl.constrain
import gg.essential.elementa.dsl.effect
import gg.essential.elementa.dsl.minus
import gg.essential.elementa.dsl.percent
import gg.essential.elementa.dsl.pixels
import gg.essential.elementa.dsl.plus
import gg.essential.elementa.dsl.toConstraint
import gg.essential.elementa.effects.OutlineEffect
import gg.essential.elementa.utils.withAlpha
import java.awt.Color

class TwentyFortyEight : Game(), RpcProvider {

private var state = TwentyFortyEightState()

private fun getHeader(): UIComponent = UIContainer().apply {
UIText("2048").constrain {
this.x = 0.pixels()
this.y = CenterConstraint()
} childOf this

val scoreBox = UIBlock().constrain {
this.color = Color.WHITE.withAlpha(0.5f).toConstraint()
this.height = 60.percent()
this.width = AspectConstraint(1.5f)
this.x = 0.pixels(alignOpposite = true)
this.y = CenterConstraint()
} childOf this

UIText(state.score.toString()).constrain {
this.x = CenterConstraint()
this.y = CenterConstraint()
this.textScale = 0.75.pixels()
} childOf scoreBox
}

private fun getBoard(): UIComponent = UIBlock().apply {
constrain {
this.color = WordlePalette.DARK_GRAY.toConstraint()
this.width = AspectConstraint()
this.height = MinConstraint(100.percent(), 200.pixels())
this.x = CenterConstraint()
this.y = CenterConstraint()
}

val container = UIBlock().constrain {
this.width = AspectConstraint()
this.height = 75.percent()
this.x = CenterConstraint()
this.y = 20.percent()
} childOf this

getHeader().constrain {
this.width = 100.percent() constrainTo container
this.height = 20.percent()
this.x = CenterConstraint()
} childOf this

for (y in 0..3) {
for (x in 0..3) {
val value = state.get(x, y)
val block = UIBlock().constrain {
this.color = TwentyFortyEightPalette.getTileColor(value).toConstraint()
this.height = 20.percent()
this.width = 20.percent()
this.x = 2.5.percent() + (x * 25).percent()
this.y = 2.5.percent() + (y * 25).percent()
} childOf container

if (value != 0) {
val text = value.toString()
UIText(text, shadow = false).constrain {
this.x = CenterConstraint()
this.y = CenterConstraint() - 1.pixels()
this.width = 20.percent()
this.height = 20.percent()
this.textScale = if (text.length > 3) 1.25.pixels() else 1.5.pixels()
this.color = TwentyFortyEightPalette.getTextColor(value).toConstraint()
} childOf block
}
}
}
}

override fun getDisplay(): UIComponent {
return UIContainer().apply {
constrain {
this.width = 100.percent()
this.height = 100.percent()
}

val board = getBoard() childOf this

if (state.won || state.lost) {
val text = if (state.won) "You won!" else "You lost!"
val fade = ((System.currentTimeMillis() - state.endingTime) / 1000.0f).coerceIn(0.0f, 0.95f)

val overlay = UIBlock().constrain {
this.color = Color.BLACK.withAlpha(fade).toConstraint()
this.height = 100.percent()
this.width = 100.percent()
} childOf board

if (fade > 0.5f) {

UIText(text).constrain {
this.x = CenterConstraint()
this.y = CenterConstraint()
} childOf overlay

val button = UIBlock().constrain {
this.color = WordlePalette.DARK_GRAY.toConstraint()
this.height = 25.pixels()
this.width = 80.pixels()
this.x = CenterConstraint()
this.y = CenterConstraint() + 50.pixels()
}.onMouseEnter {
effect(OutlineEffect(Color.WHITE, 1f))
}.onMouseLeave {
removeEffect<OutlineEffect>()
}.onMouseClick {
state = TwentyFortyEightState()
} childOf overlay

UIText("New Game").constrain {
this.x = CenterConstraint()
this.y = CenterConstraint()
} childOf button
}
}
}
}


override fun onKeyPressed(key: Int): Boolean {
val direction = KeybindSet.configPrimary().getDirection(key) ?: KeybindSet.configSecondary().getDirection(key) ?: return false
state.input(-direction.x, -direction.y)
return true
}

override fun getRpcInfo(): RpcInfo = state.getRpcInfo()
}

object TwentyFortyEightInformation : GameInformation() {
override val name: String = "2048"
override val description: String = "A game where you slide tiles to combine them"
override val factory: () -> Game = { TwentyFortyEight() }
override val supportsPictureInPicture: Boolean = true
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package gay.j10a1n15.sillygames.games.numbers

import java.awt.Color

object TwentyFortyEightPalette {

private val TEXT_COLOR_BELOW_8 = Color(119, 110, 101)
private val TEXT_COLOR_ABOVE_8 = Color(249, 246, 242)

private val TILE_0 = Color(205, 193, 180)
private val TILE_2 = Color(238, 228, 218)
private val TILE_4 = Color(237, 224, 200)
private val TILE_8 = Color(242, 177, 121)
private val TILE_16 = Color(245, 149, 99)
private val TILE_32 = Color(246, 124, 95)
private val TILE_64 = Color(246, 94, 59)
private val TILE_128 = Color(237, 207, 114)
private val TILE_256 = Color(237, 204, 97)
private val TILE_512 = Color(237, 200, 80)
private val TILE_1024 = Color(237, 197, 63)
private val TILE_2048 = Color(237, 194, 46)

fun getTileColor(value: Int) = when (value) {
2 -> TILE_2
4 -> TILE_4
8 -> TILE_8
16 -> TILE_16
32 -> TILE_32
64 -> TILE_64
128 -> TILE_128
256 -> TILE_256
512 -> TILE_512
1024 -> TILE_1024
2048 -> TILE_2048
else -> TILE_0
}

fun getTextColor(value: Int) = if (value < 8) TEXT_COLOR_BELOW_8 else TEXT_COLOR_ABOVE_8
}
126 changes: 126 additions & 0 deletions src/main/kotlin/gay/j10a1n15/sillygames/games/numbers/2048State.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
package gay.j10a1n15.sillygames.games.numbers

import gay.j10a1n15.sillygames.rpc.RpcInfo
import gay.j10a1n15.sillygames.rpc.RpcProvider
import gay.j10a1n15.sillygames.utils.SillyUtils.pad

class TwentyFortyEightState: RpcProvider {

private val rpc = RpcInfo("2048", "Score: 0 | Highest Tile: 0", System.currentTimeMillis())

private val board = Array(4) { Array(4) { Tile(0) } }

var won = false
private set
var lost = false
private set
var score = 0
private set
var endingTime = 0L
private set

init {
random()
random()
}

private fun random() {
if (board.all { row -> row.all { it.value != 0 } }) return

val x = (0..3).random()
val y = (0..3).random()
if (board[x][y].value == 0) {
board[x][y].value = 2
} else {
random()
}
}

private fun merge(dx: Int, dy: Int, simulate: Boolean = false): Boolean {
var merged = false
val xRange = if (dx == 1) 3 downTo 1 else if (dx == -1) 0..2 else 0..3
val yRange = if (dy == 1) 3 downTo 1 else if (dy == -1) 0..2 else 0..3

board.forEach { row -> row.forEach { it.merged = false } }

for (x in xRange) {
for (y in yRange) {
val current = board[x][y]
val next = board[x - dx][y - dy]
if (current.value != 0 && current.value == next.value && !current.merged && !next.merged) {
if (simulate) return true
current.value *= 2
current.merged = true
next.value = 0
score += current.value
merged = true
}
}
}
return merged
}

private fun move(dx: Int, dy: Int, simulate: Boolean = false): Boolean {
var moved = false
when {
dx == 0 -> {
val unfiltered = (0..3).map { x -> (0..3).map { y -> board[x][y] } }
val columns = unfiltered.map { it.filter { it.value != 0 } }
for (x in 0..3) {
val missing = 4 - columns[x].size
val merged = columns[x].pad(missing, dy == -1) { Tile(0) }
moved = moved or (unfiltered[x] != merged)
if (simulate && unfiltered[x] != merged) return true
for (y in 0..3) {
board[x][y] = merged[y]
}
}
}
dy == 0 -> {
val unfiltered = (0..3).map { y -> (0..3).map { x -> board[x][y] } }
val rows = unfiltered.map { it.filter { it.value != 0 } }
for (y in 0..3) {
val missing = 4 - rows[y].size
val merged = rows[y].pad(missing, dx == -1) { Tile(0) }
moved = moved or (unfiltered[y] != merged)
if (simulate && unfiltered[y] != merged) return true
for (x in 0..3) {
board[x][y] = merged[x]
}
}
}
}
return moved
}

fun input(dx: Int, dy: Int) {
if (won || lost) return
var successful = false
successful = successful or move(dx, dy)
successful = successful or merge(-dx, -dy)
successful = successful or move(dx, dy)
if (successful) random()

rpc.secondLine = "Score: $score | Highest Tile: ${board.flatMap { it.map { it.value }.toList() }.maxOrNull()}"

if (board.any { row -> row.any { it.value == 2048 } }) {
won = true
endingTime = System.currentTimeMillis()
rpc.end = endingTime
} else {
val canMerge = merge(1, 0, true) || merge(-1, 0, true) || merge(0, 1, true) || merge(0, -1, true)
val canMove = move(1, 0, true) || move(-1, 0, true) || move(0, 1, true) || move(0, -1, true)
if (!canMerge && !canMove) {
lost = true
endingTime = System.currentTimeMillis()
rpc.end = endingTime
}
}
}

fun get(x: Int, y: Int) = board[x][y].value

override fun getRpcInfo() = rpc
}

private data class Tile(var value: Int, var merged: Boolean = false)
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package gay.j10a1n15.sillygames.screens

import gay.j10a1n15.sillygames.games.GameInformation
import gay.j10a1n15.sillygames.games.SnakeInformation
import gay.j10a1n15.sillygames.games.numbers.TwentyFortyEightInformation
import gay.j10a1n15.sillygames.games.wordle.WordleInformation
import gay.j10a1n15.sillygames.utils.SillyUtils.display
import gg.essential.elementa.ElementaVersion
Expand All @@ -28,6 +29,7 @@ import java.awt.Color
val games = listOf(
SnakeInformation,
WordleInformation,
TwentyFortyEightInformation,
)

class GameSelector : WindowScreen(
Expand Down
12 changes: 12 additions & 0 deletions src/main/kotlin/gay/j10a1n15/sillygames/utils/SillyUtils.kt
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,16 @@ object SillyUtils {

@Suppress("DEPRECATION")
fun Int.getKeyCodeName() = UKeyboard.getKeyName(this, -1)

fun <T> List<T>.pad(amount: Int, left: Boolean = false, factory: () -> T): List<T> {
val result = ArrayList<T>(this.size + amount)
if (left) {
repeat(amount) { result.add(factory()) }
}
result.addAll(this)
if (!left) {
repeat(amount) { result.add(factory()) }
}
return result
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package gay.j10a1n15.sillygames.utils.essentials

import gg.essential.elementa.UIComponent
import gg.essential.elementa.constraints.RelativeConstraint

object ElementaUtils {
infix fun RelativeConstraint.constrainTo(constraint: UIComponent) = apply {
this.constrainTo = constraint
}
}

0 comments on commit acb8e6b

Please sign in to comment.