Skip to content

Commit

Permalink
feat(ghost)!: implement ghost block game feature
Browse files Browse the repository at this point in the history
BREAKING CHANGE: Facade class now accepts a game constructor for
allowing a new instance of GhostGame.

this closes #4
  • Loading branch information
Pablito2020 committed Apr 22, 2022
1 parent 0d3b289 commit b4a1840
Show file tree
Hide file tree
Showing 18 changed files with 348 additions and 115 deletions.
8 changes: 4 additions & 4 deletions src/main/kotlin/GameFacade.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import block_factory.BlockType
import block_factory.RandomBlockCreator
import game.Game
import game.GameCell
import game.normal.NormalGame
import movements.Direction
import movements.Rotation
import score.Points
Expand All @@ -11,18 +12,17 @@ import score.SimpleScoreCalculator

class GameFacade(
private val blockGenerator: BlockCreator = RandomBlockCreator(),
scoreCalculator: ScoreCalculator = SimpleScoreCalculator()
scoreCalculator: ScoreCalculator = SimpleScoreCalculator(),
private val game: Game = NormalGame(blockGenerator, scoreCalculator)
) {

private val game = Game(blockGenerator, scoreCalculator)
private var started = false

/**
* Start the game
*/
fun start() {
if (!started)
game.generateNextBlock().also { started = true}
game.generateNextBlock().also { started = true }
else
throw IllegalAccessError("Game has already started")
}
Expand Down
2 changes: 1 addition & 1 deletion src/main/kotlin/block_factory/RandomBlockCreator.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ package block_factory

import blocks.Block
import blocks.implementation.*
import game.GAME_COLUMNS
import game.normal.GAME_COLUMNS
import movements.Position

data class BlockWrapper(val block: Block, val type: BlockType)
Expand Down
89 changes: 9 additions & 80 deletions src/main/kotlin/game/Game.kt
Original file line number Diff line number Diff line change
@@ -1,96 +1,25 @@
package game

import block_factory.BlockCreator
import blocks.Block
import board.Board
import game.exceptions.BlockCanMoveDownException
import game.exceptions.EmptyCurrentBlockException
import movements.Direction
import movements.Rotation
import score.Points
import score.ScoreCalculator

internal const val GAME_COLUMNS = 10
internal const val GAME_ROWS = 20
internal const val DEFAULT_POINTS = 0
interface Game {

class Game(private val creator: BlockCreator, private val scoreCalculator: ScoreCalculator) {
fun getGrid(): List<List<GameCell>>

private val board: Board = Board(GAME_ROWS, GAME_COLUMNS)
private var block: Block? = null
private var points: Points = Points(DEFAULT_POINTS)
fun generateNextBlock()

fun getGrid(): List<List<GameCell>> {
val result = getGameCellGridFromBoard()
if (block != null)
block!!.getNeededPositions()
.forEach { result[it.row][it.column] = GameCell(block!!.getCell(), isCurrentBlockCell = true) }
return result
}
fun moveBlock(direction: Direction)

fun generateNextBlock() {
block = creator.getBlock()
}
fun rotateBlock(rotation: Rotation)

fun moveBlock(direction: Direction) {
assertBlockNotNull()
if (blockIsInValidPosition(block!!.move(direction)))
block = block!!.move(direction)
}
fun blockCanMoveDownNext(): Boolean

fun rotateBlock(rotation: Rotation) {
assertBlockNotNull()
if (blockIsInValidPosition(block!!.rotate(rotation)))
block = block!!.rotate(rotation)
}
fun writeBlockToBoard()

fun blockCanMoveDownNext(): Boolean {
assertBlockNotNull()
return blockIsInValidPosition(block!!.move(Direction.DOWN))
}
fun hasFinished(): Boolean

fun writeBlockToBoard() {
assertBlockNotNull()
if (blockCanMoveDownNext())
throw BlockCanMoveDownException("Block can move down next, so it's impossible to write it to the board")
block!!.getNeededPositions().forEach { board.writePosition(block!!.getCell(), it) }
checkIfCompletedRows()
}
fun getScore(): Points

fun hasFinished(): Boolean {
assertBlockNotNull()
return !blockIsInValidPosition(block!!)
}

fun getScore(): Points {
return points
}

private fun assertBlockNotNull() {
if (block == null)
throw EmptyCurrentBlockException("Did you initialize a block with getNextBlock()?")
}

private fun getGameCellGridFromBoard(): MutableList<MutableList<GameCell>> {
val immutableBoardGameCell = board.board.map { row -> row.map { cell -> GameCell(cell) } }
return immutableBoardGameCell.map { it.toMutableList() }.toMutableList()
}

private fun blockIsInValidPosition(block: Block) =
block.getNeededPositions().all { board.isInside(it) && board.isEmpty(it) }

private fun checkIfCompletedRows() {
if (hasCompletedRows()) {
updateScore()
board.redoBoardWithClearedCells()
}
}

private fun hasCompletedRows() = (0 until GAME_ROWS).any { row -> board.isFull(row) }

private fun getCompletedRows() = (0 until GAME_ROWS).count { row -> board.isFull(row) }

private fun updateScore() {
points = points.add(scoreCalculator.getScore(getCompletedRows()))
}
}
4 changes: 1 addition & 3 deletions src/main/kotlin/game/GameCell.kt
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
@file:Suppress("unused", "unused")

package game

import board.Cell

data class GameCell(val cell: Cell, val isCurrentBlockCell: Boolean = false)
data class GameCell(val cell: Cell, val isCurrentBlockCell: Boolean = false, val isGhostBlockCell: Boolean = false)
57 changes: 57 additions & 0 deletions src/main/kotlin/game/ghost/GhostGame.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package game.ghost

import block_factory.BlockCreator
import game.Game
import game.GameCell
import game.normal.NormalGame
import movements.Direction
import movements.Position
import movements.Rotation
import score.Points
import score.ScoreCalculator

class GhostGame(creator: BlockCreator, scoreCalculator: ScoreCalculator) : Game {

private val game = NormalGame(creator, scoreCalculator)

override fun getGrid(): List<List<GameCell>> {
val grid = game.getGrid().toMutableList().map { it.toMutableList() }
return getGridWithGhostBlock(grid)
}

override fun generateNextBlock() = game.generateNextBlock()

override fun moveBlock(direction: Direction) = game.moveBlock(direction)

override fun rotateBlock(rotation: Rotation) = game.rotateBlock(rotation)

override fun blockCanMoveDownNext(): Boolean = game.blockCanMoveDownNext()

override fun writeBlockToBoard() = game.writeBlockToBoard()

override fun hasFinished(): Boolean = game.hasFinished()

override fun getScore(): Points = game.getScore()

private fun getGridWithGhostBlock(grid: List<MutableList<GameCell>>): List<List<GameCell>> {
// get current cells of the block
val result = mutableListOf<Position>()
for ((rowI, row) in grid.withIndex()) {
for (column in row.indices) {
if (grid[rowI][column].isCurrentBlockCell)
result.add(Position(rowI, column))
}
}
if (result.isEmpty())
return grid
// get minimum row of current block
val distance = result.getRowOffset(grid)
// Write Result
for (position in result) {
val currentGhostBlock = GameCell(grid[position.row][position.column].cell, false, true)
grid[position.row + distance][position.column] = currentGhostBlock
}
return grid
}

}
17 changes: 17 additions & 0 deletions src/main/kotlin/game/ghost/utils.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package game.ghost

import board.Cell
import game.GameCell
import movements.Position

internal fun Position.getLastEmptyBlock(grid: List<List<GameCell>>): Position {
var varRow = this.row
while (varRow < grid.size && (grid[varRow][column].cell == Cell.EMPTY || grid[varRow][column].isCurrentBlockCell))
varRow += 1
return Position(varRow - 1, column)
}

internal fun List<Position>.getRowOffset(grid: List<List<GameCell>>): Int {
val minimum = this.minByOrNull{ it.getLastEmptyBlock(grid).row - it.row }
return minimum!!.getLastEmptyBlock(grid).row - minimum.row
}
98 changes: 98 additions & 0 deletions src/main/kotlin/game/normal/NormalGame.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
package game.normal

import block_factory.BlockCreator
import blocks.Block
import board.Board
import game.Game
import game.GameCell
import game.exceptions.BlockCanMoveDownException
import game.exceptions.EmptyCurrentBlockException
import movements.Direction
import movements.Rotation
import score.Points
import score.ScoreCalculator

internal const val GAME_COLUMNS = 10
internal const val GAME_ROWS = 20
internal const val DEFAULT_POINTS = 0

class NormalGame(private val creator: BlockCreator, private val scoreCalculator: ScoreCalculator): Game {

private val board: Board = Board(GAME_ROWS, GAME_COLUMNS)
private var block: Block? = null
private var points: Points = Points(DEFAULT_POINTS)

override fun getGrid(): List<List<GameCell>> {
val result = getGameCellGridFromBoard()
if (block != null)
block!!.getNeededPositions()
.forEach { result[it.row][it.column] = GameCell(block!!.getCell(), isCurrentBlockCell = true) }
return result
}

override fun generateNextBlock() {
block = creator.getBlock()
}

override fun moveBlock(direction: Direction) {
assertBlockNotNull()
if (blockIsInValidPosition(block!!.move(direction)))
block = block!!.move(direction)
}

override fun rotateBlock(rotation: Rotation) {
assertBlockNotNull()
if (blockIsInValidPosition(block!!.rotate(rotation)))
block = block!!.rotate(rotation)
}

override fun blockCanMoveDownNext(): Boolean {
assertBlockNotNull()
return blockIsInValidPosition(block!!.move(Direction.DOWN))
}

override fun writeBlockToBoard() {
assertBlockNotNull()
if (blockCanMoveDownNext())
throw BlockCanMoveDownException("Block can move down next, so it's impossible to write it to the board")
block!!.getNeededPositions().forEach { board.writePosition(block!!.getCell(), it) }
checkIfCompletedRows()
}

override fun hasFinished(): Boolean {
assertBlockNotNull()
return !blockIsInValidPosition(block!!)
}

override fun getScore(): Points {
return points
}

private fun assertBlockNotNull() {
if (block == null)
throw EmptyCurrentBlockException("Did you initialize a block with getNextBlock()?")
}

private fun getGameCellGridFromBoard(): MutableList<MutableList<GameCell>> {
val immutableBoardGameCell = board.board.map { row -> row.map { cell -> GameCell(cell) } }
return immutableBoardGameCell.map { it.toMutableList() }.toMutableList()
}

private fun blockIsInValidPosition(block: Block) =
block.getNeededPositions().all { board.isInside(it) && board.isEmpty(it) }

private fun checkIfCompletedRows() {
if (hasCompletedRows()) {
updateScore()
board.redoBoardWithClearedCells()
}
}

private fun hasCompletedRows() = (0 until GAME_ROWS).any { row -> board.isFull(row) }

private fun getCompletedRows() = (0 until GAME_ROWS).count { row -> board.isFull(row) }

private fun updateScore() {
points = points.add(scoreCalculator.getScore(getCompletedRows()))
}
}
Loading

0 comments on commit b4a1840

Please sign in to comment.