Skip to content

Instantly share code, notes, and snippets.

@RezMike

RezMike/Block.kt

Created Sep 25, 2020
Embed
What would you like to do?
Code for "KorGE Tutorial - Writing 2048 game. Step 4 - Data saving"
import Number.*
import com.soywiz.korge.view.*
import com.soywiz.korim.color.*
fun Container.block(number: Number) = Block(number).addTo(this)
class Block(val number: Number) : Container() {
init {
roundRect(cellSize, cellSize, 5.0, color = number.color)
val textColor = when (number) {
ZERO, ONE -> Colors.BLACK
else -> Colors.WHITE
}
text(number.value.toString(), textSizeFor(number), textColor, font).apply {
centerBetween(0.0, 0.0, cellSize, cellSize)
}
}
}
private fun textSizeFor(number: Number) = when (number) {
ZERO, ONE, TWO, THREE, FOUR, FIVE -> cellSize / 2
SIX, SEVEN, EIGHT -> cellSize * 4 / 9
NINE, TEN, ELEVEN, TWELVE -> cellSize * 2 / 5
THIRTEEN, FOURTEEN, FIFTEEN -> cellSize * 7 / 20
SIXTEEN -> cellSize * 3 / 10
}
import com.soywiz.kds.iterators.*
class History(from: String?, private val onUpdate: (History) -> Unit) {
class Element(val numberIds: IntArray, val score: Int)
private val history = mutableListOf<Element>()
val currentElement: Element get() = history.last()
init {
from?.split(';')?.fastForEach {
val element = elementFromString(it)
history.add(element)
}
}
private fun elementFromString(string: String): Element {
val numbers = string.split(',').map { it.toInt() }
if (numbers.size != 17) throw IllegalArgumentException("Incorrect history")
return Element(IntArray(16) { numbers[it] }, numbers[16])
}
fun add(numberIds: IntArray, score: Int) {
history.add(Element(numberIds, score))
onUpdate(this)
}
fun undo(): Element {
if (history.size > 1) {
history.removeAt(history.size - 1)
onUpdate(this)
}
return history.last()
}
fun clear() {
history.clear()
onUpdate(this)
}
fun isEmpty() = history.isEmpty()
override fun toString(): String {
return history.joinToString(";") {
it.numberIds.joinToString(",") + "," + it.score
}
}
}
import com.soywiz.klock.*
import com.soywiz.korev.*
import com.soywiz.korge.*
import com.soywiz.korge.animate.*
import com.soywiz.korge.html.*
import com.soywiz.korge.input.*
import com.soywiz.korge.service.storage.*
import com.soywiz.korge.tween.*
import com.soywiz.korge.ui.*
import com.soywiz.korge.view.*
import com.soywiz.korim.color.*
import com.soywiz.korim.font.*
import com.soywiz.korim.format.*
import com.soywiz.korio.async.*
import com.soywiz.korio.async.ObservableProperty
import com.soywiz.korio.file.std.*
import com.soywiz.korma.geom.*
import com.soywiz.korma.geom.vector.*
import com.soywiz.korma.interpolation.*
import kotlin.properties.*
import kotlin.random.*
var cellSize: Double = 0.0
var fieldSize: Double = 0.0
var leftIndent: Double = 0.0
var topIndent: Double = 0.0
var font: BitmapFont by Delegates.notNull()
fun columnX(number: Int) = leftIndent + 10 + (cellSize + 10) * number
fun rowY(number: Int) = topIndent + 10 + (cellSize + 10) * number
var map = PositionMap()
val blocks = mutableMapOf<Int, Block>()
var history: History by Delegates.notNull()
fun numberFor(blockId: Int) = blocks[blockId]!!.number
fun deleteBlock(blockId: Int) = blocks.remove(blockId)!!.removeFromParent()
val score = ObservableProperty(0)
val best = ObservableProperty(0)
var freeId = 0
var isAnimationRunning = false
var isGameOver = false
suspend fun main() = Korge(width = 480, height = 640, title = "2048", bgcolor = RGBA(253, 247, 240)) {
font = resourcesVfs["clear_sans.fnt"].readBitmapFont()
val storage = views.storage
history = History(storage.getOrNull("history")) {
storage["history"] = it.toString()
}
best.update(storage.getOrNull("best")?.toInt() ?: 0)
score.observe {
if (it > best.value) best.update(it)
}
best.observe {
storage["best"] = it.toString()
}
cellSize = views.virtualWidth / 5.0
fieldSize = 50 + 4 * cellSize
leftIndent = (views.virtualWidth - fieldSize) / 2
topIndent = 150.0
val bgField = roundRect(fieldSize, fieldSize, 5.0, color = Colors["#b9aea0"]) {
position(leftIndent, topIndent)
}
graphics {
position(leftIndent, topIndent)
fill(Colors["#cec0b2"]) {
for (i in 0..3) {
for (j in 0..3) {
roundRect(10 + (10 + cellSize) * i, 10 + (10 + cellSize) * j, cellSize, cellSize, 5.0)
}
}
}
}
val bgLogo = roundRect(cellSize, cellSize, 5.0, color = Colors["#edc403"]) {
position(leftIndent, 30.0)
}
text("2048", cellSize * 0.5, Colors.WHITE, font).centerOn(bgLogo)
val bgBest = roundRect(cellSize * 1.5, cellSize * 0.8, 5.0, color = Colors["#bbae9e"]) {
alignRightToRightOf(bgField)
alignTopToTopOf(bgLogo)
}
text("BEST", cellSize * 0.25, RGBA(239, 226, 210), font) {
centerXOn(bgBest)
alignTopToTopOf(bgBest, 5.0)
}
text(best.value.toString(), cellSize * 0.5, Colors.WHITE, font) {
setTextBounds(Rectangle(0.0, 0.0, bgBest.width, cellSize - 24.0))
format = format.copy(align = Html.Alignment.MIDDLE_CENTER)
alignTopToTopOf(bgBest, 12.0)
centerXOn(bgBest)
best.observe {
text = it.toString()
}
}
val bgScore = roundRect(cellSize * 1.5, cellSize * 0.8, 5.0, color = Colors["#bbae9e"]) {
alignRightToLeftOf(bgBest, 24.0)
alignTopToTopOf(bgBest)
}
text("SCORE", cellSize * 0.25, RGBA(239, 226, 210), font) {
centerXOn(bgScore)
alignTopToTopOf(bgScore, 5.0)
}
text(score.value.toString(), cellSize * 0.5, Colors.WHITE, font) {
setTextBounds(Rectangle(0.0, 0.0, bgScore.width, cellSize - 24.0))
format = format.copy(align = Html.Alignment.MIDDLE_CENTER)
centerXOn(bgScore)
alignTopToTopOf(bgScore, 12.0)
score.observe {
text = it.toString()
}
}
val btnSize = cellSize * 0.3
val restartImg = resourcesVfs["restart.png"].readBitmap()
val undoImg = resourcesVfs["undo.png"].readBitmap()
val restartBlock = container {
val background = roundRect(btnSize, btnSize, 5.0, color = RGBA(185, 174, 160))
image(restartImg) {
size(btnSize * 0.8, btnSize * 0.8)
centerOn(background)
}
alignTopToBottomOf(bgBest, 5.0)
alignRightToRightOf(bgField)
onClick {
this@Korge.restart()
}
}
val undoBlock = container {
val background = roundRect(btnSize, btnSize, 5.0, color = RGBA(185, 174, 160))
image(undoImg) {
size(btnSize * 0.6, btnSize * 0.6)
centerOn(background)
}
alignTopToTopOf(restartBlock)
alignRightToLeftOf(restartBlock, 5.0)
onClick {
this@Korge.restoreField(history.undo())
}
}
if (!history.isEmpty()) {
restoreField(history.currentElement)
} else {
generateBlockAndSave()
}
onKeyDown {
when (it.key) {
Key.LEFT -> moveBlocksTo(Direction.LEFT)
Key.RIGHT -> moveBlocksTo(Direction.RIGHT)
Key.UP -> moveBlocksTo(Direction.TOP)
Key.DOWN -> moveBlocksTo(Direction.BOTTOM)
else -> Unit
}
}
onSwipe(20.0) {
when (it.direction) {
SwipeDirection.LEFT -> moveBlocksTo(Direction.LEFT)
SwipeDirection.RIGHT -> moveBlocksTo(Direction.RIGHT)
SwipeDirection.TOP -> moveBlocksTo(Direction.TOP)
SwipeDirection.BOTTOM -> moveBlocksTo(Direction.BOTTOM)
}
}
}
fun Stage.moveBlocksTo(direction: Direction) {
if (isAnimationRunning) return
if (!map.hasAvailableMoves()) {
if (!isGameOver) {
isGameOver = true
showGameOver {
isGameOver = false
restart()
}
}
return
}
val moves = mutableListOf<Pair<Int, Position>>()
val merges = mutableListOf<Triple<Int, Int, Position>>()
val newMap = calculateNewMap(map.copy(), direction, moves, merges)
if (map != newMap) {
isAnimationRunning = true
showAnimation(moves, merges) {
map = newMap
generateBlockAndSave()
isAnimationRunning = false
val points = merges.sumBy { numberFor(it.first).value }
score.update(score.value + points)
}
}
}
fun Stage.showAnimation(
moves: List<Pair<Int, Position>>,
merges: List<Triple<Int, Int, Position>>,
onEnd: () -> Unit
) = launchImmediately {
animateSequence {
parallel {
moves.forEach { (id, pos) ->
blocks[id]!!.moveTo(columnX(pos.x), rowY(pos.y), 0.15.seconds, Easing.LINEAR)
}
merges.forEach { (id1, id2, pos) ->
sequence {
parallel {
blocks[id1]!!.moveTo(columnX(pos.x), rowY(pos.y), 0.15.seconds, Easing.LINEAR)
blocks[id2]!!.moveTo(columnX(pos.x), rowY(pos.y), 0.15.seconds, Easing.LINEAR)
}
block {
val nextNumber = numberFor(id1).next()
deleteBlock(id1)
deleteBlock(id2)
createNewBlockWithId(id1, nextNumber, pos)
}
sequenceLazy {
animateScale(blocks[id1]!!)
}
}
}
}
block {
onEnd()
}
}
}
fun Animator.animateScale(block: Block) {
val x = block.x
val y = block.y
val scale = block.scale
tween(
block::x[x - 4],
block::y[y - 4],
block::scale[scale + 0.1],
time = 0.1.seconds,
easing = Easing.LINEAR
)
tween(
block::x[x],
block::y[y],
block::scale[scale],
time = 0.1.seconds,
easing = Easing.LINEAR
)
}
fun calculateNewMap(
map: PositionMap,
direction: Direction,
moves: MutableList<Pair<Int, Position>>,
merges: MutableList<Triple<Int, Int, Position>>
): PositionMap {
val newMap = PositionMap()
val startIndex = when (direction) {
Direction.LEFT, Direction.TOP -> 0
Direction.RIGHT, Direction.BOTTOM -> 3
}
var columnRow = startIndex
fun newPosition(line: Int) = when (direction) {
Direction.LEFT -> Position(columnRow++, line)
Direction.RIGHT -> Position(columnRow--, line)
Direction.TOP -> Position(line, columnRow++)
Direction.BOTTOM -> Position(line, columnRow--)
}
for (line in 0..3) {
var curPos = map.getNotEmptyPositionFrom(direction, line)
columnRow = startIndex
while (curPos != null) {
val newPos = newPosition(line)
val curId = map[curPos.x, curPos.y]
map[curPos.x, curPos.y] = -1
val nextPos = map.getNotEmptyPositionFrom(direction, line)
val nextId = nextPos?.let { map[it.x, it.y] }
//two blocks are equal
if (nextId != null && numberFor(curId) == numberFor(nextId)) {
//merge these blocks
map[nextPos.x, nextPos.y] = -1
newMap[newPos.x, newPos.y] = curId
merges += Triple(curId, nextId, newPos)
} else {
//add old block
newMap[newPos.x, newPos.y] = curId
moves += Pair(curId, newPos)
}
curPos = map.getNotEmptyPositionFrom(direction, line)
}
}
return newMap
}
fun Container.showGameOver(onRestart: () -> Unit) = container {
val format = TextFormat(
color = RGBA(0, 0, 0),
size = 40,
font = Html.FontFace.Bitmap(font)
)
val skin = TextSkin(
normal = format,
over = format.copy(color = RGBA(90, 90, 90)),
down = format.copy(color = RGBA(120, 120, 120))
)
fun restart() {
this@container.removeFromParent()
onRestart()
}
position(leftIndent, topIndent)
roundRect(fieldSize, fieldSize, 5.0, color = Colors["#FFFFFF33"])
text("Game Over", 60.0, Colors.BLACK, font) {
centerBetween(0.0, 0.0, fieldSize, fieldSize)
y -= 60
}
uiText("Try again", 120.0, 35.0, skin) {
centerBetween(0.0, 0.0, fieldSize, fieldSize)
y += 20
onClick { restart() }
}
onKeyDown {
when (it.key) {
Key.ENTER, Key.SPACE -> restart()
else -> Unit
}
}
}
fun Container.restart() {
map = PositionMap()
blocks.values.forEach { it.removeFromParent() }
blocks.clear()
score.update(0)
history.clear()
generateBlockAndSave()
}
fun Container.restoreField(history: History.Element) {
map.forEach { if (it != -1) deleteBlock(it) }
map = PositionMap()
score.update(history.score)
freeId = 0
val numbers = history.numberIds.map {
if (it >= 0 && it < Number.values().size)
Number.values()[it]
else null
}
numbers.forEachIndexed { i, number ->
if (number != null) {
val newId = createNewBlock(number, Position(i % 4, i / 4))
map[i % 4, i / 4] = newId
}
}
}
fun Container.generateBlockAndSave() {
val position = map.getRandomFreePosition() ?: return
val number = if (Random.nextDouble() < 0.9) Number.ZERO else Number.ONE
val newId = createNewBlock(number, position)
map[position.x, position.y] = newId
history.add(map.toNumberIds(), score.value)
}
fun Container.createNewBlock(number: Number, position: Position): Int {
val id = freeId++
createNewBlockWithId(id, number, position)
return id
}
fun Container.createNewBlockWithId(id: Int, number: Number, position: Position) {
blocks[id] = block(number).position(columnX(position.x), rowY(position.y))
}
import com.soywiz.korim.color.RGBA
enum class Number(val value: Int, val color: RGBA) {
ZERO(2, RGBA(240, 228, 218)),
ONE(4, RGBA(236, 224, 201)),
TWO(8, RGBA(255, 178, 120)),
THREE(16, RGBA(254, 150, 92)),
FOUR(32, RGBA(247, 123, 97)),
FIVE(64, RGBA(235, 88, 55)),
SIX(128, RGBA(236, 220, 146)),
SEVEN(256, RGBA(240, 212, 121)),
EIGHT(512, RGBA(244, 206, 96)),
NINE(1024, RGBA(248, 200, 71)),
TEN(2048, RGBA(256, 194, 46)),
ELEVEN(4096, RGBA(104, 130, 249)),
TWELVE(8192, RGBA(51, 85, 247)),
THIRTEEN(16384, RGBA(10, 47, 222)),
FOURTEEN(32768, RGBA(9, 43, 202)),
FIFTEEN(65536, RGBA(181, 37, 188)),
SIXTEEN(131072, RGBA(166, 34, 172));
fun next() = values()[(ordinal + 1) % values().size]
}
import com.soywiz.kds.*
import kotlin.random.*
class Position(val x: Int, val y: Int)
enum class Direction {
LEFT, RIGHT, TOP, BOTTOM
}
class PositionMap(private val array: IntArray2 = IntArray2(4, 4, -1)) {
private fun getOrNull(x: Int, y: Int) = if (array.get(x, y) != -1) Position(x, y) else null
private fun getNumber(x: Int, y: Int) = array.tryGet(x, y)?.let { blocks[it]?.number?.ordinal ?: -1 } ?: -1
fun hasAvailableMoves(): Boolean {
array.each { x, y, _ ->
if (hasAdjacentEqualPosition(x, y)) return true
}
return false
}
private fun hasAdjacentEqualPosition(x: Int, y: Int) = getNumber(x, y).let {
it == getNumber(x - 1, y) || it == getNumber(x + 1, y) || it == getNumber(x, y - 1) || it == getNumber(x, y + 1)
}
fun getNotEmptyPositionFrom(direction: Direction, line: Int): Position? {
when (direction) {
Direction.LEFT -> for (i in 0..3) getOrNull(i, line)?.let { return it }
Direction.RIGHT -> for (i in 3 downTo 0) getOrNull(i, line)?.let { return it }
Direction.TOP -> for (i in 0..3) getOrNull(line, i)?.let { return it }
Direction.BOTTOM -> for (i in 3 downTo 0) getOrNull(line, i)?.let { return it }
}
return null
}
fun getRandomFreePosition(): Position? {
val quantity = array.count { it == -1 }
if (quantity == 0) return null
val chosen = Random.nextInt(quantity)
var current = -1
array.each { x, y, value ->
if (value == -1) {
current++
if (current == chosen) {
return Position(x, y)
}
}
}
return null
}
operator fun get(x: Int, y: Int) = array[x, y]
operator fun set(x: Int, y: Int, value: Int) {
array[x, y] = value
}
fun toNumberIds() = IntArray(16) { getNumber(it % 4, it / 4) }
fun forEach(action: (Int) -> Unit) { array.forEach(action) }
fun copy() = PositionMap(array.copy(data = array.data.copyOf()))
override fun equals(other: Any?): Boolean {
return (other is PositionMap) && this.array.data.contentEquals(other.array.data)
}
override fun hashCode() = array.hashCode()
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.