-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* 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
1 parent
580a551
commit acb8e6b
Showing
6 changed files
with
355 additions
and
0 deletions.
There are no files selected for viewing
166 changes: 166 additions & 0 deletions
166
src/main/kotlin/gay/j10a1n15/sillygames/games/numbers/2048.kt
This file contains 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
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 | ||
} |
39 changes: 39 additions & 0 deletions
39
src/main/kotlin/gay/j10a1n15/sillygames/games/numbers/2048Palette.kt
This file contains 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
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
126
src/main/kotlin/gay/j10a1n15/sillygames/games/numbers/2048State.kt
This file contains 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
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) |
This file contains 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
This file contains 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
10 changes: 10 additions & 0 deletions
10
src/main/kotlin/gay/j10a1n15/sillygames/utils/essentials/ElementaUtils.kt
This file contains 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
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 | ||
} | ||
} |