Skip to content

Instantly share code, notes, and snippets.

@jayrambhia
Last active April 21, 2023 14:11
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • Save jayrambhia/8260e059ec3c4e287acdedc3ebf322a7 to your computer and use it in GitHub Desktop.
Save jayrambhia/8260e059ec3c4e287acdedc3ebf322a7 to your computer and use it in GitHub Desktop.
Battleship game in Kotlin
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="com.fenchtose.battleship.GameActivity">
<android.support.v7.widget.RecyclerView
android:id="@+id/recyclerview"
android:background="@color/cell_background"
android:layout_width="match_parent"
android:layout_height="wrap_content"
tools:listitem="@layout/square_cell_itemview"/>
<TextView
android:id="@+id/gamestate_info"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="bottom"
android:layout_margin="16dp"
android:textSize="21sp"
android:gravity="center"
tools:text="Your turn!"/>
</FrameLayout>
class BattleshipActivity: AppCompatActivity() {
private lateinit var gridAdpater: UiCellAdapter
private lateinit var myBoard: Board
private lateinit var otherBoard: Board
private val width: Int = 10
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_game);
gridAdapter = UiCellAdapter(this, { playMove(it.point) })
gridAdapter.setHasStableIds(true)
val recyclerview = findViewById<RecyclerView>(R.id.recyclerview).apply {
layoutManager = GridLayoutManager(this@BattleshipActivity, width)
adapter = gridAdpater
}
setupGame()
updateUI()
}
private fun updateUI() {
val cells = convert(myBoard)
adapter.cells.clear()
adapter.cells.addAll(cells)
adapter.cells.notifyDataSetChanged()
}
private fun playMove(p: Point) {
// check if already played
if (p in otherBoard.hits || p in otherBoard.misses) {
return
}
var missed = true
var hitShip: Ship? = null
for (ship in otherBoard.ships) {
if (p in ship) {
// we hit the ship
hitShip = ship.copy(hits=ship.hits.plus(p))
missed = false
break
}
}
if (missed) {
myBoard = myBoard.copy(misses = myBoard.misses.plus(p))
otherBoard = otherBoard.copy(
ships = if (hitShip) != null ? otherBoard.ships.update(hitShip),
opponentMisses = otherBoard.opponentMisses.plus(p))
}
updateUI()
}
private fun convert(board: Board): List<Cell> {
val cells = ArrayList<Cell>(board.width * board.height)
for (i in 0 until board.height) {
for (j in 0 until board.width) {
val point = Point(j, i)
cells.add(Cell(
point = point,
hasShip = board.ships.contains(point),
direction = board.ships.getShipDirection(point),
userHit = board.hits.contains(point),
userMissed = board.misses.contains(point),
opponentMissed = board.opponentMisses.contains(point),
opponentHit = board.opponentHits.contains(point)
))
}
}
return cells
}
private fun setupGame() {
myBoard = Board(0, User("Player 1"), width, width, getRandomShips(0))
otherBoard = Board(1, User("Player 2"), width, width, getRandomShips(1))
}
private fun getRandomShips(startId: Int) {
val ships = ArrayList<Ship>()
for (i in 1..5) {
ships.add(Ship(startId + i, i, Point(i, i), (if (i%2 == 0) Direction.HORIZONTAL else Direction.VERTICAL )))
}
return ships
}
}
data class Point(val col: Int, val row: Int) {
fun isValid() = col >= 0 && row >= 0
operator fun plus(wd: WeighedDirection) = when (wd.d) {
Direction.HORIZONTAL -> Point(col + wd.len, row)
Direction.VERTICAL -> Point(col, row + wd.len)
}
}
enum class Direction {
HORIZONTAL, VERTICAL;
operator fun times(n: Int): WeighedDirection {
return WeighedDirection(this, n)
}
}
data class WeighedDirection(val d: Direction, val len: Int)
data class Ship(
val id: Int,
val size: Int, val start: Point, val direction: Direction,
val hits: Set<Point> = setOf()) {
val end = start + direction*(size-1)
val destroyed = hits.size == size
// check for overlaps
operator fun contains(other: Ship): Boolean {
if (other.direction == direction) {
return other.start in this || other.end in this
}
val vertical = if (other.direction == Direction.VERTICAL) other else this
val horizontal = if (other.direction == Direction.HORIZONTAL) other else this
return horizontal.start.row in vertical.start.row..vertical.end.row
&& vertical.start.col in horizontal.start.col..horizontal.end.col
}
operator fun contains(p: Point): Boolean {
return when(direction) {
Direction.HORIZONTAL -> start.row == p.row && start.col <= p.col && end.col >= p.col
Direction.VERTICAL -> start.col == p.col && start.row <= p.row && end.row >= p.row
}
}
}
data class Board(
val id: Int, val user: User,
val width: Int, val height: Int,
val ships: List<Ship> = listOf(),
val hits: Set<Point> = setOf(),
val misses: Set<Point> = setOf(),
val opponentHits: Set<Point> = setOf(),
val opponentMisses: Set<Point> = setOf()) {
val activeShips = ships.filter { !it.destroyed }.map { it.id }
val destroyedShips = ships.filter { it.destroyed }.map { it.id }
val lost = ships.isNotEmpty() && activeShips.isEmpty()
fun fits(ship: Ship) = ship.start in this && ship.end in this
fun isOverlap(ship: Ship): Boolean {
return ships.any { ship in it }
}
operator fun contains(p: Point) = p.isValid() && p.col < width && p.row < height
}
// Extension function to update the ship (based on id) and return new list
fun List<Ship>.update(ship: Ship): List<Ship> {
var index = -1
forEachIndexed { i, s ->
if (s.id == ship.id) {
index = i
return@forEachIndexed
}
}
if (index != -1) {
val mutable = ArrayList(this)
mutable.removeAt(index)
mutable.add(index, ship)
return mutable
}
return this
}
// Extension function to check if any of the ships are at the given point
fun List<Ship>.contains(p: Point): Boolean {
forEach {
if (p in it) {
return true
}
}
return false
}
// Extension function to check the direction of the ship at the given point
fun List<Ship>.getShipDirection(p: Point): Direction? {
forEach {
if (p in it) {
return it.direction
}
}
return null
}
data class Cell(
val point: Point,
val hasShip: Boolean = false,
val direction: Direction? = null,
val userHit: Boolean = false,
val userMissed: Boolean = false,
val opponentHit: Boolean = false,
val opponentMissed: Boolean = false)
class SquareCell: View {
private var cell: Cell? = null
private val shipPaint: Paint
private val shipHitPaint: Paint
private val hitPaint: Paint
private val missedPaint: Paint
private val opponentHitPaint: Paint
private val opponentMissedPaint: Paint
private val borderPaint: Paint
private val rect: Rect = Rect()
constructor(context: Context) : this(context, null)
constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, 0)
constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr) {
shipPaint = initPaint(R.color.cell_my_ship, cap = Paint.Cap.ROUND)
shipHitPaint = initPaint(R.color.cell_my_ship_hit)
hitPaint = initPaint(R.color.cell_ship_hit)
opponentMissedPaint = initPaint(R.color.cell_opponent_miss)
opponentHitPaint = initPaint(R.color.cell_ship_hit)
missedPaint = initPaint(R.color.cell_ship_miss)
borderPaint = initPaint(R.color.cell_border, Paint.Style.STROKE, 2f)
if (isInEditMode) {
hasShip = true
shipDirection = Direction.HORIZONTAL
}
}
private fun initPaint(@ColorRes color: Int, style: Paint.Style = Paint.Style.FILL, width: Float? = null, cap: Paint.Cap? = null): Paint {
return Paint(Paint.ANTI_ALIAS_FLAG).apply {
this.color = ContextCompat.getColor(context, color)
this.style = style
width?.let { this.strokeWidth = it }
cap?.let { this.strokeCap = it }
}
}
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
setMeasuredDimension(widthMeasureSpec, widthMeasureSpec)
}
override fun onDraw(canvas: Canvas) {
rect.left = 0
rect.top = 0
rect.bottom = canvas.height
rect.right = canvas.width
canvas.drawRect(rect, borderPaint)
cell?.apply {
if (hasShip && direction != null) {
when (direction) {
Direction.HORIZONTAL -> {
rect.bottom = canvas.height - 64
rect.right = canvas.width
}
Direction.VERTICAL -> {
rect.bottom = canvas.height
rect.right = canvas.width - 64
}
}
val paint = if (!opponentHit) shipPaint else shipHitPaint
canvas.drawRect(rect, paint)
}
if (userHit) {
canvas.drawCircle((canvas.width - 24).toFloat(), (canvas.height - 24).toFloat(), 16f, hitPaint)
} else if (userMissed) {
canvas.drawCircle((canvas.width - 24).toFloat(), (canvas.height - 24).toFloat(), 16f, missedPaint)
}
if (opponentHit) {
canvas.drawCircle(24f, 24f, 16f, opponentHitPaint)
} else if (opponentMissed) {
canvas.drawCircle(24f, 24f, 16f, opponentMissedPaint)
}
}
}
fun bind(cell: Cell) {
this.cell = cell
invalidate()
}
}
class UiCellAdapter(context: Context, private val onClick: ((Cell) -> Unit)): RecyclerView.Adapter<RecyclerView.ViewHolder>() {
val cells = ArrayList<Cell>()
private val inflater = LayoutInflater.from(context)
override fun getItemCount(): Int {
return cells.size
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
return UiCellViewHolder(inflater.inflate(R.layout.square_cell_itemview, parent, false), onClick)
}
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
holder as UiCellViewHolder
holder.bind(cells[position])
}
override fun getItemId(position: Int): Long {
return cells[position].hashCode().toLong()
}
class UiCellViewHolder(itemView: View, onClick: (Cell) -> Unit) : RecyclerView.ViewHolder(itemView) {
private val view = itemView as SquareCell
private var cell: Cell? = null
init {
view.setOnClickListener {
cell?.let(onClick)
}
}
fun bind(cell: Cell) {
this.cell = cell
view.bind(cell)
}
}
}
fun Board.reduceOffense(action: Action): Board {
return when(action) {
is GeneratedAction.MissedMove -> copy(misses = misses.plus(action.point))
is GeneratedAction.DefinitiveAction -> copy(hits = hits.plus(action.point))
else -> this
}
}
fun Board.reduceDefense(action: Action): Board {
return when(action) {
is GeneratedAction.MissedMove -> copy(opponentMisses = opponentMisses.plus(action.point))
is GeneratedAction.DefinitiveAction -> {
val ship = ships.getById(action.ship.id)
if (ship != null) {
val updated = ship.reduce(action)
copy(opponentHits = opponentHits.plus(action.point), ships = ships.update(updated))
} else {
this
}
}
else -> this
}
}
fun Board.reduceSetup(action: Action): Board {
return when(action) {
is AddShip -> copy(ships = ships.add(action.ship))
else -> this
}
}
fun Ship.reduce(action: Action): Ship {
return when(action) {
is GeneratedAction.DefinitiveAction -> copy(hits = hits.plus(action.point))
else -> this
}
}
fun destroyMiddleware(state: GameState, action: Action, dispatch: Dispatch, next: Next<GameState>): Action {
if (action is GeneratedAction.DefinitiveAction.HitMove) {
if (action.ship.hits.size + 1 == action.ship.size) {
return next(state, GeneratedAction.DefinitiveAction.DestroyShip(action.offense, action.defense, action.point, action.ship), dispatch)
}
}
return next(state, action, dispatch)
}
fun lostMiddleware(state: GameState, action: Action, dispatch: Dispatch, next: Next<GameState>): Action {
if (action is GeneratedAction.DefinitiveAction.DestroyShip) {
if (state.boardById(action.defense).activeShips.size == 1) {
return next(state, GeneratedAction.DefinitiveAction.LostGame(action.offense, action.defense, action.point, action.ship), dispatch)
}
}
return next(state, action, dispatch)
}
class GameActivity : AppCompatActivity() {
private lateinit var adapter: UiCellAdapter
private lateinit var gamestateInfo: TextView
private lateinit var handler: Handler
private lateinit var store: Gamestore
private lateinit var myBoard: Board
private lateinit var otherBoard: Board
private var unsubscribe: Unsubscribe? = null
private var dispatch: Dispatch? = null
private var gameOver: Boolean = false
private val width: Int = 10
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_game)
handler = Handler()
gamestateInfo = findViewById(R.id.gamestate_info)
val recyclerview = findViewById<RecyclerView>(R.id.recyclerview)
adapter = UiCellAdapter(this, {
if (gameOver) {
return@UiCellAdapter
}
dispatch?.invoke(Move(myBoard.id, otherBoard.id, it.point))
})
adapter.setHasStableIds(true)
recyclerview.layoutManager = GridLayoutManager(this, width)
recyclerview.adapter = adapter
adapter.notifyDataSetChanged()
myBoard = Board(0, User("Player 1"), width, width)
otherBoard = Board(1, User("Player 2"), width, width)
val initState = GameState(
board1 = myBoard,
board2 = otherBoard,
lastPlayed = otherBoard.id
)
store = Gamestore(initState)
setupGame()
}
override fun onResume() {
super.onResume()
unsubscribe = store.subscribe({ state, dispatch ->
this.dispatch = dispatch
myBoard = state.board1
otherBoard = state.board2
val cells = generateCells(myBoard)
adapter.cells.clear()
adapter.cells.addAll(cells)
adapter.notifyDataSetChanged()
gamestateInfo.text = if (state.lastPlayed == otherBoard.id) "Your turn" else "Player 2 turn"
gameOver = state.gameOver
if (state.gameOver) {
gamestateInfo.text = "Game over!"
}
if(state.lastPlayed == myBoard.id && !state.gameOver) {
handler.postDelayed({
playOther()
}, 300)
}
})
}
override fun onPause() {
super.onPause()
unsubscribe?.invoke()
}
private fun setupGame() {
for (i in 1..5) {
store.dispatch(AddShip(myBoard.id, Ship(i, i, Point(i, i), (if (i%2 == 0) Direction.HORIZONTAL else Direction.VERTICAL ))))
store.dispatch(AddShip(otherBoard.id, Ship(10 + i, i, Point(i, i), (if (i%2 == 0) Direction.HORIZONTAL else Direction.VERTICAL ))))
}
}
private fun playOther() {
val random = Random()
while(true) {
val point = Point(random.nextInt(width), random.nextInt(width))
if (point !in otherBoard.hits && point !in otherBoard.misses) {
dispatch?.invoke(Move(otherBoard.id, myBoard.id, point))
break
}
}
}
private fun generateCells(board: Board): ArrayList<Cell> {
val cells = ArrayList<Cell>(board.width * board.height)
for (i in 0 until board.height) {
for (j in 0 until board.width) {
val point = Point(j, i)
cells.add(Cell(
point = point,
hasShip = board.ships.contains(point),
direction = board.ships.getShipDirection(point),
userHit = board.hits.contains(point),
userMissed = board.misses.contains(point),
opponentMissed = board.opponentMisses.contains(point),
opponentHit = board.opponentHits.contains(point)
))
}
}
return cells
}
}
fun GameState.reduceSetup(action: Action): GameState {
return when(action) {
is AddShip -> reduceChildState(this, boardById(action.offense), action, Board::reduceSetup, { state, board -> state.updateBoard(board) })
else -> this
}
}
fun GameState.reduceGameplay(action: Action): GameState {
return when(action) {
is GeneratedAction -> {
val offense = boardById(action.offense).reduceOffense(action)
val defense = boardById(action.defense).reduceDefense(action)
copy(board1 = whichBoard(board1, offense, defense),
board2 = whichBoard(board2, offense, defense),
gameOver = action is GeneratedAction.DefinitiveAction.LostGame)
}
is SwitchAction -> {
reduceGameplay(action.last).copy(lastPlayed = action.offense)
}
else -> this
}
}
fun<State, Child> reduceChildState(
state: State,
child: Child,
action: Action,
reducer: Reducer<Child>,
onReduced: (State, Child) -> State): State {
val reduced = reducer.invoke(child, action)
if (reduced === child) {
return state
}
return onReduced(state, reduced)
}
private fun whichBoard(board: Board, offense: Board, defense: Board): Board {
return when(board.id) {
offense.id -> offense
defense.id -> defense
else -> board
}
}
fun gameSetupMiddleware(state: GameState, action: Action, dispatch: Dispatch, next: Next<GameState>): Action {
if (action is AddShip) {
val board = state.boardById(action.offense)
if (!board.fits(action.ship) || board.isOverlap(action.ship)) {
return next(state, AddShipInvalid(action.offense, action.ship), dispatch)
}
}
return next(state, action, dispatch)
}
data class GameState(
val board1: Board,
val board2: Board,
val lastPlayed: Int,
val gameOver: Boolean = false) {
fun hasBoardById(id: Int): Boolean {
return when(id) {
board1.id, board2.id -> true
else -> false
}
}
fun boardById(id: Int): Board {
return when (id) {
board1.id -> board1
board2.id -> board2
else -> throw RuntimeException("No such board with id: $id")
}
}
fun updateBoard(board: Board): GameState {
return when(board.id) {
board1.id -> copy(board1 = board)
board2.id -> copy(board2 = board)
else -> this
}
}
}
class Gamestore(initialSate: GameState): SimpleStore<GameState>(
initialState = initialSate,
middlewares = listOf(
::stateValidityMiddleware,
::gameSetupMiddleware,
::moveMiddleware,
::hitMiddleware,
::destroyMiddleware,
::lostMiddleware,
::switcherMiddleware
),
reducers = listOf(
GameState::reduceSetup,
GameState::reduceGameplay
)
)
data class AddShip(val offense: Int, val ship: Ship): Action
data class AddShipInvalid(val offense: Int, val ship: Ship): Action
sealed class GeneratedAction(val offense: Int, val defense: Int, val point: Point): Action {
class InvalidMove(offense: Int, defense: Int, point: Point): GeneratedAction(offense, defense, point)
class PlayMove(offense: Int, defense: Int, point: Point): GeneratedAction(offense, defense, point)
class MissedMove(offense: Int, defense: Int, point: Point): GeneratedAction(offense, defense, point)
sealed class DefinitiveAction(offense: Int, defense: Int, point: Point, val ship: Ship): GeneratedAction(offense, defense, point) {
class HitMove(offense: Int, defense: Int, point: Point, ship: Ship) : DefinitiveAction(offense, defense, point, ship)
class DestroyShip(offense: Int, defense: Int, point: Point, ship: Ship) : DefinitiveAction(offense, defense, point, ship)
class LostGame(offense: Int, defense: Int, point: Point, ship: Ship) : DefinitiveAction(offense, defense, point, ship)
}
}
data class SwitchAction(val offense: Int, val defense: Int, val last: GeneratedAction): Action
object InvalidState: Action
fun hitMiddleware(state: GameState, action: Action, dispatch: Dispatch, next: Next<GameState>): Action {
if (action is GeneratedAction.PlayMove) {
val defense = state.boardById(action.defense)
for (id in defense.activeShips) {
defense.ships.getById(id)?.let {
if (action.point in it) {
return next(state, GeneratedAction.DefinitiveAction.HitMove(action.offense, action.defense, action.point, it), dispatch)
}
}
}
return next(state, GeneratedAction.MissedMove(action.offense, action.defense, action.point), dispatch)
}
return next(state, action, dispatch)
}
fun moveMiddleware(state: GameState, action: Action, dispatch: Dispatch, next: Next<GameState>): Action {
if (action is Move) {
val offense = state.boardById(action.offense)
if (offense.hits.contains(action.point) || offense.misses.contains(action.point)) {
return next(state, GeneratedAction.InvalidMove(action.offense, action.defense, action.point), dispatch)
}
return next(state, GeneratedAction.PlayMove(action.offense, action.defense, action.point), dispatch)
}
return next(state, action, dispatch)
}
<?xml version="1.0" encoding="utf-8"?>
<com.fenchtose.battleship.ui.SquareCell
xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content">
</com.fenchtose.battleship.ui.SquareCell>
fun stateValidityMiddleware(state: GameState, action: Action, dispatch: Dispatch, next: Next<GameState>): Action {
when(action) {
is Move -> {
if (!state.hasBoardById(action.offense) || !state.hasBoardById(action.defense)) {
return InvalidState
}
}
is GeneratedAction -> {
if (!state.hasBoardById(action.offense) || !state.hasBoardById(action.defense)) {
return InvalidState
}
}
is AddShip -> {
if (!state.hasBoardById(action.offense)) {
return InvalidState
}
}
}
return next(state, action, dispatch)
}
fun switcherMiddleware(state: GameState, action: Action, dispatch: Dispatch, next: Next<GameState>): Action {
if (action is GeneratedAction && action !is GeneratedAction.InvalidMove) {
return next(state, SwitchAction(action.offense, action.defense, action), dispatch)
}
return next(state, action, dispatch)
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment