Added obligatory assignments
This commit is contained in:
43
src/assignment/week2/mastermind/evaluateGuess.kt
Normal file
43
src/assignment/week2/mastermind/evaluateGuess.kt
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
package mastermind
|
||||||
|
|
||||||
|
data class Evaluation(val rightPosition: Int, val wrongPosition: Int)
|
||||||
|
|
||||||
|
enum class Match {
|
||||||
|
RIGHT_POSITION,
|
||||||
|
WRONG_POSITION,
|
||||||
|
}
|
||||||
|
|
||||||
|
data class CharMatch(val char: Char, var match: Match?)
|
||||||
|
|
||||||
|
fun evaluateGuess(secret: String, guess: String): Evaluation {
|
||||||
|
val secretMatches = createMatches(secret)
|
||||||
|
|
||||||
|
for ((index, guessChar) in guess.withIndex()) {
|
||||||
|
if (secretMatches[index].char == guessChar) {
|
||||||
|
if (secretMatches[index].match == Match.WRONG_POSITION) {
|
||||||
|
for ((index, secretChar) in secret.drop(index + 1).withIndex()) {
|
||||||
|
if (secretChar == guessChar) {
|
||||||
|
secretMatches[index].match = Match.WRONG_POSITION
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
secretMatches[index].match = Match.RIGHT_POSITION
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
for ((index, secretChar) in secret.withIndex()) {
|
||||||
|
if (secretMatches[index].match == null && secretChar == guessChar) {
|
||||||
|
secretMatches[index].match = Match.WRONG_POSITION
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Evaluation(
|
||||||
|
secretMatches.filter { it.match == Match.RIGHT_POSITION }.size,
|
||||||
|
secretMatches.filter { it.match == Match.WRONG_POSITION }.size
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun createMatches(secret: String): List<CharMatch> = secret.map { CharMatch(it, null) }
|
||||||
|
|
57
src/assignment/week2/mastermind/playMastermind.kt
Normal file
57
src/assignment/week2/mastermind/playMastermind.kt
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
package mastermind
|
||||||
|
|
||||||
|
import kotlin.random.Random
|
||||||
|
|
||||||
|
val ALPHABET = 'A'..'F'
|
||||||
|
const val CODE_LENGTH = 4
|
||||||
|
|
||||||
|
fun main() {
|
||||||
|
val differentLetters = false
|
||||||
|
playMastermind(differentLetters)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun playMastermind(
|
||||||
|
differentLetters: Boolean,
|
||||||
|
secret: String = generateSecret(differentLetters)
|
||||||
|
) {
|
||||||
|
var evaluation: Evaluation
|
||||||
|
|
||||||
|
do {
|
||||||
|
print("Your guess: ")
|
||||||
|
var guess = readLine()!!
|
||||||
|
while (hasErrorsInInput(guess)) {
|
||||||
|
println("Incorrect input: $guess. " +
|
||||||
|
"It should consist of $CODE_LENGTH characters from $ALPHABET. " +
|
||||||
|
"Please try again.")
|
||||||
|
guess = readLine()!!
|
||||||
|
}
|
||||||
|
evaluation = evaluateGuess(secret, guess)
|
||||||
|
if (evaluation.isComplete()) {
|
||||||
|
println("The guess is correct!")
|
||||||
|
} else {
|
||||||
|
println("Right positions: ${evaluation.rightPosition}; " +
|
||||||
|
"wrong positions: ${evaluation.wrongPosition}.")
|
||||||
|
}
|
||||||
|
|
||||||
|
} while (!evaluation.isComplete())
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Evaluation.isComplete(): Boolean = rightPosition == CODE_LENGTH
|
||||||
|
|
||||||
|
fun hasErrorsInInput(guess: String): Boolean {
|
||||||
|
val possibleLetters = ALPHABET.toSet()
|
||||||
|
return guess.length != CODE_LENGTH || guess.any { it !in possibleLetters }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun generateSecret(differentLetters: Boolean): String {
|
||||||
|
val chars = ALPHABET.toMutableList()
|
||||||
|
return buildString {
|
||||||
|
for (i in 1..CODE_LENGTH) {
|
||||||
|
val letter = chars[Random.nextInt(chars.size)]
|
||||||
|
append(letter)
|
||||||
|
if (differentLetters) {
|
||||||
|
chars.remove(letter)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
25
src/assignment/week3/nicestring/NiceString.kt
Normal file
25
src/assignment/week3/nicestring/NiceString.kt
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
package nicestring
|
||||||
|
|
||||||
|
fun String.isNice(): Boolean {
|
||||||
|
return listOf(
|
||||||
|
::containsNotNiceSubstring,
|
||||||
|
::containsAtLeastThreeVowels,
|
||||||
|
::containsDoubleLetter
|
||||||
|
)
|
||||||
|
.count { it() } >= 2
|
||||||
|
}
|
||||||
|
|
||||||
|
fun String.containsNotNiceSubstring(): Boolean {
|
||||||
|
val illegalSubStrings = listOf("bu", "ba", "be")
|
||||||
|
return this.zipWithNext()
|
||||||
|
.map { (first, second) -> "$first$second" }
|
||||||
|
.none { it in illegalSubStrings }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun String.containsAtLeastThreeVowels(): Boolean {
|
||||||
|
val vovels = "aeiou".toCharArray()
|
||||||
|
return this.count { char -> char in vovels } >= 3
|
||||||
|
}
|
||||||
|
|
||||||
|
fun String.containsDoubleLetter(): Boolean = this.zipWithNext()
|
||||||
|
.any { (first, second) -> first == second }
|
24
src/assignment/week3/taxipark/TaxiPark.kt
Normal file
24
src/assignment/week3/taxipark/TaxiPark.kt
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
package taxipark
|
||||||
|
|
||||||
|
data class TaxiPark(
|
||||||
|
val allDrivers: Set<Driver>,
|
||||||
|
val allPassengers: Set<Passenger>,
|
||||||
|
val trips: List<Trip>)
|
||||||
|
|
||||||
|
data class Driver(val name: String)
|
||||||
|
data class Passenger(val name: String)
|
||||||
|
|
||||||
|
data class Trip(
|
||||||
|
val driver: Driver,
|
||||||
|
val passengers: Set<Passenger>,
|
||||||
|
// the trip duration in minutes
|
||||||
|
val duration: Int,
|
||||||
|
// the trip distance in km
|
||||||
|
val distance: Double,
|
||||||
|
// the percentage of discount (in 0.0..1.0 if not null)
|
||||||
|
val discount: Double? = null
|
||||||
|
) {
|
||||||
|
// the total cost of the trip
|
||||||
|
val cost: Double
|
||||||
|
get() = (1 - (discount ?: 0.0)) * (duration + distance)
|
||||||
|
}
|
80
src/assignment/week3/taxipark/TaxiParkTask.kt
Normal file
80
src/assignment/week3/taxipark/TaxiParkTask.kt
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
package taxipark
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Task #1. Find all the drivers who performed no trips.
|
||||||
|
*/
|
||||||
|
fun TaxiPark.findFakeDrivers(): Set<Driver> =
|
||||||
|
this.allDrivers
|
||||||
|
.filter { driver -> this.trips.none { trip -> trip.driver == driver } }
|
||||||
|
.toSet()
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Task #2. Find all the clients who completed at least the given number of trips.
|
||||||
|
*/
|
||||||
|
fun TaxiPark.findFaithfulPassengers(minTrips: Int): Set<Passenger> =
|
||||||
|
this.allPassengers
|
||||||
|
.filter { passenger -> this.trips.count { trip -> trip.passengers.contains(passenger) } >= minTrips }
|
||||||
|
.toSet()
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Task #3. Find all the passengers, who were taken by a given driver more than once.
|
||||||
|
*/
|
||||||
|
fun TaxiPark.findFrequentPassengers(driver: Driver): Set<Passenger> =
|
||||||
|
this.allPassengers
|
||||||
|
.filter { passenger ->
|
||||||
|
this.trips
|
||||||
|
.filter { trip -> trip.passengers.contains(passenger) }
|
||||||
|
.count { trip -> trip.driver == driver } > 1
|
||||||
|
}
|
||||||
|
.toSet()
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Task #4. Find the passengers who had a discount for majority of their trips.
|
||||||
|
*/
|
||||||
|
fun TaxiPark.findSmartPassengers(): Set<Passenger> = this.allPassengers
|
||||||
|
.filter(fun(passenger: Passenger): Boolean {
|
||||||
|
val (discounted, nonDiscounted) = this.trips
|
||||||
|
.filter { it.passengers.contains(passenger) }
|
||||||
|
.partition { it.discount != null }
|
||||||
|
|
||||||
|
if (nonDiscounted.isEmpty()) {
|
||||||
|
return discounted.isNotEmpty()
|
||||||
|
}
|
||||||
|
return (discounted.size.toDouble()) / nonDiscounted.size > 1.0
|
||||||
|
})
|
||||||
|
.toSet()
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Task #5. Find the most frequent trip duration among minute periods 0..9, 10..19, 20..29, and so on.
|
||||||
|
* Return any period if many are the most frequent, return `null` if there're no trips.
|
||||||
|
*/
|
||||||
|
fun TaxiPark.findTheMostFrequentTripDurationPeriod(): IntRange? {
|
||||||
|
val maxDuration = this.trips
|
||||||
|
.map { trip -> trip.duration / 10 }
|
||||||
|
.groupBy { it }
|
||||||
|
.maxByOrNull { it.value.size }
|
||||||
|
|
||||||
|
val startRange = maxDuration?.let { it.key * 10 }
|
||||||
|
return startRange?.let { it.rangeTo(it + 9) }
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Task #6.
|
||||||
|
* Check whether 20% of the drivers contribute 80% of the income.
|
||||||
|
*/
|
||||||
|
fun TaxiPark.checkParetoPrinciple(): Boolean {
|
||||||
|
if (this.trips.isEmpty()) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
val totalIncome = this.trips.sumOf(Trip::cost)
|
||||||
|
|
||||||
|
val top20Percent = this.trips
|
||||||
|
.groupBy { it.driver }
|
||||||
|
.values
|
||||||
|
.map { trips -> trips.sumOf(Trip::cost) }
|
||||||
|
.sortedDescending()
|
||||||
|
.take((this.allDrivers.size * 0.2).toInt())
|
||||||
|
.sum()
|
||||||
|
|
||||||
|
return top20Percent >= totalIncome * 0.8
|
||||||
|
}
|
41
src/assignment/week4/board/Board.kt
Normal file
41
src/assignment/week4/board/Board.kt
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
package board
|
||||||
|
|
||||||
|
data class Cell(val i: Int, val j: Int) {
|
||||||
|
override fun toString()= "($i, $j)"
|
||||||
|
}
|
||||||
|
|
||||||
|
enum class Direction {
|
||||||
|
UP, DOWN, RIGHT, LEFT;
|
||||||
|
|
||||||
|
fun reversed() = when (this) {
|
||||||
|
UP -> DOWN
|
||||||
|
DOWN -> UP
|
||||||
|
RIGHT -> LEFT
|
||||||
|
LEFT -> RIGHT
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SquareBoard {
|
||||||
|
val width: Int
|
||||||
|
|
||||||
|
fun getCellOrNull(i: Int, j: Int): Cell?
|
||||||
|
fun getCell(i: Int, j: Int): Cell
|
||||||
|
|
||||||
|
fun getAllCells(): Collection<Cell>
|
||||||
|
|
||||||
|
fun getRow(i: Int, jRange: IntProgression): List<Cell>
|
||||||
|
fun getColumn(iRange: IntProgression, j: Int): List<Cell>
|
||||||
|
|
||||||
|
fun Cell.getNeighbour(direction: Direction): Cell?
|
||||||
|
}
|
||||||
|
|
||||||
|
interface GameBoard<T> : SquareBoard {
|
||||||
|
|
||||||
|
operator fun get(cell: Cell): T?
|
||||||
|
operator fun set(cell: Cell, value: T?)
|
||||||
|
|
||||||
|
fun filter(predicate: (T?) -> Boolean): Collection<Cell>
|
||||||
|
fun find(predicate: (T?) -> Boolean): Cell?
|
||||||
|
fun any(predicate: (T?) -> Boolean): Boolean
|
||||||
|
fun all(predicate: (T?) -> Boolean): Boolean
|
||||||
|
}
|
80
src/assignment/week4/board/BoardImpl.kt
Normal file
80
src/assignment/week4/board/BoardImpl.kt
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
package board
|
||||||
|
|
||||||
|
import kotlin.collections.component1
|
||||||
|
|
||||||
|
fun createSquareBoard(width: Int): SquareBoard = SquareBoardImpl(width)
|
||||||
|
fun <T> createGameBoard(width: Int): GameBoard<T> = GameBoardImpl(width)
|
||||||
|
|
||||||
|
open class SquareBoardImpl(override val width: Int) : SquareBoard {
|
||||||
|
|
||||||
|
protected val cells: List<Cell>
|
||||||
|
|
||||||
|
init {
|
||||||
|
val range = 1..width
|
||||||
|
cells = range
|
||||||
|
.flatMap { first -> range.map { second -> Cell(first, second) } }
|
||||||
|
.distinct()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getCellOrNull(i: Int, j: Int): Cell? =
|
||||||
|
cells.firstOrNull { cell -> cell.i == i && cell.j == j }
|
||||||
|
|
||||||
|
|
||||||
|
override fun getCell(i: Int, j: Int): Cell =
|
||||||
|
getCellOrNull(i, j) ?: throw IllegalArgumentException("Range is out of bounds")
|
||||||
|
|
||||||
|
|
||||||
|
override fun getAllCells(): Collection<Cell> = cells
|
||||||
|
|
||||||
|
override fun getRow(i: Int, jRange: IntProgression): List<Cell> {
|
||||||
|
val cells = cells.filter { cell -> cell.i == i && cell.j in jRange }
|
||||||
|
return if (jRange.isIncreasing()) cells else cells.sortedByDescending { it.j }
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getColumn(iRange: IntProgression, j: Int): List<Cell> {
|
||||||
|
val cells = cells.filter { cell -> cell.j == j && cell.i in iRange }
|
||||||
|
return if (iRange.isIncreasing()) cells else cells.sortedByDescending { it.i }
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun Cell.getNeighbour(direction: Direction): Cell? = when (direction) {
|
||||||
|
Direction.UP -> getCellOrNull(i - 1, j)
|
||||||
|
Direction.RIGHT -> getCellOrNull(i, j + 1)
|
||||||
|
Direction.DOWN -> getCellOrNull(i + 1, j)
|
||||||
|
Direction.LEFT -> getCellOrNull(i, j - 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun IntProgression.isIncreasing() = step > 0
|
||||||
|
}
|
||||||
|
|
||||||
|
class GameBoardImpl<T>(width: Int) : SquareBoardImpl(width), GameBoard<T> {
|
||||||
|
private val values: MutableMap<Cell, T?>
|
||||||
|
|
||||||
|
init {
|
||||||
|
val map = mutableMapOf<Cell, T?>()
|
||||||
|
cells.forEach { map.put(it, null) }
|
||||||
|
values = map
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun get(cell: Cell): T? = values[cell]
|
||||||
|
|
||||||
|
override fun set(cell: Cell, value: T?) {
|
||||||
|
values[cell] = value
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun filter(predicate: (T?) -> Boolean): Collection<Cell> = values
|
||||||
|
.filterValues(predicate)
|
||||||
|
.keys
|
||||||
|
|
||||||
|
override fun find(predicate: (T?) -> Boolean): Cell? = values
|
||||||
|
.entries
|
||||||
|
.firstOrNull { (_, value) -> predicate(value) }
|
||||||
|
?.key
|
||||||
|
|
||||||
|
override fun any(predicate: (T?) -> Boolean): Boolean = values
|
||||||
|
.entries
|
||||||
|
.any { (_, value) -> predicate(value) }
|
||||||
|
|
||||||
|
override fun all(predicate: (T?) -> Boolean): Boolean = values
|
||||||
|
.entries
|
||||||
|
.all { (_, value) -> predicate(value) }
|
||||||
|
}
|
11
src/assignment/week5/games/game/Game.kt
Normal file
11
src/assignment/week5/games/game/Game.kt
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
package games.game
|
||||||
|
|
||||||
|
import board.Direction
|
||||||
|
|
||||||
|
interface Game {
|
||||||
|
fun initialize()
|
||||||
|
fun canMove(): Boolean
|
||||||
|
fun hasWon(): Boolean
|
||||||
|
fun processMove(direction: Direction)
|
||||||
|
operator fun get(i: Int, j: Int): Int?
|
||||||
|
}
|
90
src/assignment/week5/games/game2048/Game2048.kt
Normal file
90
src/assignment/week5/games/game2048/Game2048.kt
Normal file
@ -0,0 +1,90 @@
|
|||||||
|
package games.game2048
|
||||||
|
|
||||||
|
import board.Cell
|
||||||
|
import board.Direction
|
||||||
|
import board.GameBoard
|
||||||
|
import board.createGameBoard
|
||||||
|
import games.game.Game
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Your task is to implement the game 2048 https://en.wikipedia.org/wiki/2048_(video_game).
|
||||||
|
* Implement the utility methods below.
|
||||||
|
*
|
||||||
|
* After implementing it you can try to play the game running 'PlayGame2048'.
|
||||||
|
*/
|
||||||
|
fun newGame2048(initializer: Game2048Initializer<Int> = RandomGame2048Initializer): Game =
|
||||||
|
Game2048(initializer)
|
||||||
|
|
||||||
|
class Game2048(private val initializer: Game2048Initializer<Int>) : Game {
|
||||||
|
private val board = createGameBoard<Int?>(4)
|
||||||
|
|
||||||
|
override fun initialize() {
|
||||||
|
repeat(2) {
|
||||||
|
board.addNewValue(initializer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun canMove() = board.any { it == null }
|
||||||
|
|
||||||
|
override fun hasWon() = board.any { it == 2048 }
|
||||||
|
|
||||||
|
override fun processMove(direction: Direction) {
|
||||||
|
if (board.moveValues(direction)) {
|
||||||
|
board.addNewValue(initializer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun get(i: Int, j: Int): Int? = board.run { get(getCell(i, j)) }
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Add a new value produced by 'initializer' to a specified cell in a board.
|
||||||
|
*/
|
||||||
|
fun GameBoard<Int?>.addNewValue(initializer: Game2048Initializer<Int>) {
|
||||||
|
val (cell, value) = initializer.nextValue(this) ?: return
|
||||||
|
this[cell] = value
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Update the values stored in a board,
|
||||||
|
* so that the values were "moved" in a specified rowOrColumn only.
|
||||||
|
* Use the helper function 'moveAndMergeEqual' (in Game2048Helper.kt).
|
||||||
|
* The values should be moved to the beginning of the row (or column),
|
||||||
|
* in the same manner as in the function 'moveAndMergeEqual'.
|
||||||
|
* Return 'true' if the values were moved and 'false' otherwise.
|
||||||
|
*/
|
||||||
|
fun GameBoard<Int?>.moveValuesInRowOrColumn(rowOrColumn: List<Cell>): Boolean {
|
||||||
|
val values = rowOrColumn.map { this[it] }
|
||||||
|
val merged = values.moveAndMergeEqual { it * 2 }
|
||||||
|
|
||||||
|
for ((index, cell) in rowOrColumn.withIndex()) {
|
||||||
|
this[cell] = merged.getOrNull(index)
|
||||||
|
}
|
||||||
|
|
||||||
|
return merged.mapIndexed { index, value -> value != values[index] }.any { it }
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Update the values stored in a board,
|
||||||
|
* so that the values were "moved" to the specified direction
|
||||||
|
* following the rules of the 2048 game .
|
||||||
|
* Use the 'moveValuesInRowOrColumn' function above.
|
||||||
|
* Return 'true' if the values were moved and 'false' otherwise.
|
||||||
|
*/
|
||||||
|
fun GameBoard<Int?>.moveValues(direction: Direction): Boolean = when (direction) {
|
||||||
|
Direction.UP -> 1.rangeTo(width)
|
||||||
|
.map { j -> moveValuesInRowOrColumn(getColumn(1..width, j)) }
|
||||||
|
.any { it }
|
||||||
|
|
||||||
|
Direction.RIGHT -> 1.rangeTo(width)
|
||||||
|
.map { i -> moveValuesInRowOrColumn(getRow(i, width downTo 1)) }
|
||||||
|
.any { it }
|
||||||
|
|
||||||
|
Direction.DOWN -> 1.rangeTo(width)
|
||||||
|
.map { j -> moveValuesInRowOrColumn(getColumn(width downTo 1, j)) }
|
||||||
|
.any { it }
|
||||||
|
|
||||||
|
Direction.LEFT -> 1.rangeTo(width)
|
||||||
|
.map { i -> moveValuesInRowOrColumn(getRow(i, 1..width)) }
|
||||||
|
.any { it }
|
||||||
|
}
|
41
src/assignment/week5/games/game2048/Game2048Helper.kt
Normal file
41
src/assignment/week5/games/game2048/Game2048Helper.kt
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
package games.game2048
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This function moves all the non-null elements to the beginning of the list
|
||||||
|
* (by removing nulls) and merges equal elements.
|
||||||
|
* The parameter 'merge' specifies the way how to merge equal elements:
|
||||||
|
* it returns a new element that should be present in the resulting list
|
||||||
|
* instead of two merged elements.
|
||||||
|
*
|
||||||
|
* If the function 'merge("a")' returns "aa",
|
||||||
|
* then the function 'moveAndMergeEqual' transforms the input in the following way:
|
||||||
|
* a, a, b -> aa, b
|
||||||
|
* a, null -> a
|
||||||
|
* b, null, a, a -> b, aa
|
||||||
|
* a, a, null, a -> aa, a
|
||||||
|
* a, null, a, a -> aa, a
|
||||||
|
*
|
||||||
|
* You can find more examples in 'TestGame2048Helper'.
|
||||||
|
*/
|
||||||
|
fun <T : Any> List<T?>.moveAndMergeEqual(merge: (T) -> T): List<T> {
|
||||||
|
val mutableList = mutableListOf<T>()
|
||||||
|
val oldList = filterNotNull()
|
||||||
|
|
||||||
|
var index = 0
|
||||||
|
while (true) {
|
||||||
|
if (index >= oldList.size) break
|
||||||
|
|
||||||
|
val first = oldList[index]
|
||||||
|
val second = oldList.getOrNull(index + 1)
|
||||||
|
|
||||||
|
if (first == second) {
|
||||||
|
index += 2
|
||||||
|
mutableList.add(merge(first))
|
||||||
|
} else {
|
||||||
|
index += 1
|
||||||
|
mutableList.add(first)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return mutableList
|
||||||
|
}
|
||||||
|
|
29
src/assignment/week5/games/game2048/Game2048Initializer.kt
Normal file
29
src/assignment/week5/games/game2048/Game2048Initializer.kt
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
package games.game2048
|
||||||
|
|
||||||
|
import board.Cell
|
||||||
|
import board.GameBoard
|
||||||
|
import kotlin.random.Random
|
||||||
|
|
||||||
|
interface Game2048Initializer<T> {
|
||||||
|
/*
|
||||||
|
* Specifies the cell and the value that should be added to this cell.
|
||||||
|
*/
|
||||||
|
fun nextValue(board: GameBoard<T?>): Pair<Cell, T>?
|
||||||
|
}
|
||||||
|
|
||||||
|
object RandomGame2048Initializer : Game2048Initializer<Int> {
|
||||||
|
private fun generateRandomStartValue(): Int =
|
||||||
|
if (Random.nextInt(10) == 9) 4 else 2
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Generate a random value and a random cell among free cells
|
||||||
|
* that given value should be added to.
|
||||||
|
* The value should be 2 for 90% cases, and 4 for the rest of the cases.
|
||||||
|
* Use the 'generateRandomStartValue' function above.
|
||||||
|
* If the board is full return null.
|
||||||
|
*/
|
||||||
|
override fun nextValue(board: GameBoard<Int?>): Pair<Cell, Int>? = board
|
||||||
|
.filter { it == null }
|
||||||
|
.randomOrNull()
|
||||||
|
?.to(generateRandomStartValue())
|
||||||
|
}
|
98
src/assignment/week5/games/gameOfFifteen/GameOfFifteen.kt
Normal file
98
src/assignment/week5/games/gameOfFifteen/GameOfFifteen.kt
Normal file
@ -0,0 +1,98 @@
|
|||||||
|
package games.gameOfFifteen
|
||||||
|
|
||||||
|
import board.Cell
|
||||||
|
import board.Direction
|
||||||
|
import board.GameBoard
|
||||||
|
import board.createGameBoard
|
||||||
|
import games.game.Game
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Implement the Game of Fifteen (https://en.wikipedia.org/wiki/15_puzzle).
|
||||||
|
* When you finish, you can play the game by executing 'PlayGameOfFifteen'.
|
||||||
|
*/
|
||||||
|
fun newGameOfFifteen(initializer: GameOfFifteenInitializer = RandomGameInitializer()): Game {
|
||||||
|
val initialPermutation = initializer.initialPermutation
|
||||||
|
val board = createGameBoard<Int?>(4)
|
||||||
|
board.getAllCells().forEachIndexed { index, cell ->
|
||||||
|
if (index < initialPermutation.size) {
|
||||||
|
board[cell] = initialPermutation[index]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return GameOfFifteen(board)
|
||||||
|
}
|
||||||
|
|
||||||
|
class GameOfFifteen(val board: GameBoard<Int?>) : Game {
|
||||||
|
|
||||||
|
override fun initialize() {
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun canMove(): Boolean = !hasWon()
|
||||||
|
|
||||||
|
override fun hasWon(): Boolean {
|
||||||
|
val cells = board.getAllCells()
|
||||||
|
var previous: Int? = null
|
||||||
|
for (cell in cells) {
|
||||||
|
val current = board[cell]
|
||||||
|
if (cell == cells.first()) {
|
||||||
|
previous = current
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if (cell == cells.last() && board[cell] == null) break
|
||||||
|
if (previous == null || current != previous + 1) return false
|
||||||
|
previous = current
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun processMove(direction: Direction) {
|
||||||
|
board.moveValues(direction)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun get(i: Int, j: Int): Int? {
|
||||||
|
return board[Cell(i, j)]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun GameBoard<Int?>.moveValuesInRowOrColumn(rowOrColumn: List<Cell>): Boolean {
|
||||||
|
val values = rowOrColumn.map { this[it] }
|
||||||
|
val moved = values.move()
|
||||||
|
|
||||||
|
for ((index, cell) in rowOrColumn.withIndex()) {
|
||||||
|
this[cell] = moved.getOrNull(index)
|
||||||
|
}
|
||||||
|
|
||||||
|
return moved.mapIndexed { index, value -> value != values[index] }.any { it }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun GameBoard<Int?>.moveValues(direction: Direction): Boolean = when (direction) {
|
||||||
|
Direction.UP -> 1.rangeTo(width)
|
||||||
|
.map { j -> moveValuesInRowOrColumn(getColumn(width downTo 1, j)) }
|
||||||
|
.any { it }
|
||||||
|
|
||||||
|
Direction.RIGHT -> 1.rangeTo(width)
|
||||||
|
.map { i -> moveValuesInRowOrColumn(getRow(i, 1..width)) }
|
||||||
|
.any { it }
|
||||||
|
|
||||||
|
Direction.DOWN -> 1.rangeTo(width)
|
||||||
|
.map { j -> moveValuesInRowOrColumn(getColumn(1..width, j)) }
|
||||||
|
.any { it }
|
||||||
|
|
||||||
|
Direction.LEFT -> 1.rangeTo(width)
|
||||||
|
.map { i -> moveValuesInRowOrColumn(getRow(i, width downTo 1)) }
|
||||||
|
.any { it }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Move single element
|
||||||
|
*/
|
||||||
|
fun <T : Any> List<T?>.move(): List<T?> {
|
||||||
|
val mutableList = this.toMutableList()
|
||||||
|
val nullIndex = mutableList.indexOf(null)
|
||||||
|
if (nullIndex != -1) {
|
||||||
|
val previousValue = mutableList[nullIndex - 1]
|
||||||
|
mutableList[nullIndex] = previousValue
|
||||||
|
mutableList[nullIndex - 1] = null
|
||||||
|
return mutableList
|
||||||
|
}
|
||||||
|
return mutableList
|
||||||
|
}
|
@ -0,0 +1,15 @@
|
|||||||
|
package games.gameOfFifteen
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This function should return the parity of the permutation.
|
||||||
|
* true - the permutation is even
|
||||||
|
* false - the permutation is odd
|
||||||
|
* https://en.wikipedia.org/wiki/Parity_of_a_permutation
|
||||||
|
|
||||||
|
* If the game of fifteen is started with the wrong parity, you can't get the correct result
|
||||||
|
* (numbers sorted in the right order, empty cell at last).
|
||||||
|
* Thus the initial permutation should be correct.
|
||||||
|
*/
|
||||||
|
fun isEven(permutation: List<Int>): Boolean = permutation.flatMapIndexed { i, first ->
|
||||||
|
((i + 1) until permutation.size).filter { j -> first > permutation[j] }
|
||||||
|
}.count() % 2 == 0
|
@ -0,0 +1,23 @@
|
|||||||
|
package games.gameOfFifteen
|
||||||
|
|
||||||
|
interface GameOfFifteenInitializer {
|
||||||
|
/*
|
||||||
|
* Even permutation of numbers 1..15
|
||||||
|
* used to initialized the first 15 cells on a board.
|
||||||
|
* The last cell is empty.
|
||||||
|
*/
|
||||||
|
val initialPermutation: List<Int>
|
||||||
|
}
|
||||||
|
|
||||||
|
class RandomGameInitializer : GameOfFifteenInitializer {
|
||||||
|
/*
|
||||||
|
* Generate a random permutation from 1 to 15.
|
||||||
|
* `shuffled()` function might be helpful.
|
||||||
|
* If the permutation is not even, make it even (for instance,
|
||||||
|
* by swapping two numbers).
|
||||||
|
*/
|
||||||
|
override val initialPermutation by lazy {
|
||||||
|
(1..15).shuffled()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
110
src/assignment/week5/games/ui/PlayGame.kt
Normal file
110
src/assignment/week5/games/ui/PlayGame.kt
Normal file
@ -0,0 +1,110 @@
|
|||||||
|
// drawing based on https://github.com/bulenkov/2048
|
||||||
|
package games.ui
|
||||||
|
|
||||||
|
import board.Direction
|
||||||
|
import games.game.Game
|
||||||
|
import java.awt.*
|
||||||
|
import java.awt.event.KeyAdapter
|
||||||
|
import java.awt.event.KeyEvent
|
||||||
|
import javax.swing.JFrame
|
||||||
|
import javax.swing.JPanel
|
||||||
|
import javax.swing.WindowConstants
|
||||||
|
|
||||||
|
class PlayGame(val game: Game, val settings: GameSettings) : JPanel() {
|
||||||
|
init {
|
||||||
|
isFocusable = true
|
||||||
|
addKeyListener(object : KeyAdapter() {
|
||||||
|
override fun keyPressed(e: KeyEvent) {
|
||||||
|
if (game.hasWon() == false && game.canMove()) {
|
||||||
|
val direction = when (e.keyCode) {
|
||||||
|
KeyEvent.VK_LEFT -> Direction.LEFT
|
||||||
|
KeyEvent.VK_RIGHT -> Direction.RIGHT
|
||||||
|
KeyEvent.VK_DOWN -> Direction.DOWN
|
||||||
|
KeyEvent.VK_UP -> Direction.UP
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
if (direction != null) {
|
||||||
|
game.processMove(direction)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
repaint()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
game.initialize()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun paint(g: Graphics) {
|
||||||
|
super.paint(g)
|
||||||
|
g.color = settings.backgroundColor
|
||||||
|
g.fillRect(0, 0, this.size.width, this.size.height)
|
||||||
|
for (y in 1..4) {
|
||||||
|
for (x in 1..4) {
|
||||||
|
drawTile(g as Graphics2D, game[y, x] ?: 0, x - 1, y - 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun offsetCoors(arg: Int): Int {
|
||||||
|
return arg * (TILES_MARGIN + TILE_SIZE) + TILES_MARGIN
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun drawTile(g: Graphics2D, value: Int, x: Int, y: Int) {
|
||||||
|
g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON)
|
||||||
|
g.setRenderingHint(RenderingHints.KEY_STROKE_CONTROL, RenderingHints.VALUE_STROKE_NORMALIZE)
|
||||||
|
|
||||||
|
val xOffset = offsetCoors(x)
|
||||||
|
val yOffset = offsetCoors(y)
|
||||||
|
g.color = settings.getBackgroundColor(value)
|
||||||
|
g.fillRoundRect(xOffset, yOffset, TILE_SIZE, TILE_SIZE, 14, 14)
|
||||||
|
g.color = settings.getForegroundColor(value)
|
||||||
|
val size = if (value < 100) 36 else if (value < 1000) 32 else 24
|
||||||
|
val font = Font(FONT_NAME, Font.BOLD, size)
|
||||||
|
g.font = font
|
||||||
|
|
||||||
|
val s = value.toString()
|
||||||
|
val fm = getFontMetrics(font)
|
||||||
|
|
||||||
|
val w = fm.stringWidth(s)
|
||||||
|
val h = -fm.getLineMetrics(s, g).baselineOffsets[2].toInt()
|
||||||
|
|
||||||
|
if (value != 0)
|
||||||
|
g.drawString(s, xOffset + (TILE_SIZE - w) / 2, yOffset + TILE_SIZE - (TILE_SIZE - h) / 2 - 2)
|
||||||
|
|
||||||
|
if (game.hasWon() || game.canMove() == false) {
|
||||||
|
g.color = Color(255, 255, 255, 30)
|
||||||
|
g.fillRect(0, 0, width, height)
|
||||||
|
g.color = Color(78, 139, 202)
|
||||||
|
g.font = Font(FONT_NAME, Font.BOLD, 48)
|
||||||
|
if (game.hasWon()) {
|
||||||
|
g.drawString("You won!", 68, 150)
|
||||||
|
}
|
||||||
|
if (!game.canMove()) {
|
||||||
|
g.drawString("Game over!", 45, 160)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
g.font = Font(FONT_NAME, Font.PLAIN, 18)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private val FONT_NAME = "Arial"
|
||||||
|
private val TILE_SIZE = 64
|
||||||
|
private val TILES_MARGIN = 16
|
||||||
|
|
||||||
|
abstract class GameSettings(val name: String, val backgroundColor: Color) {
|
||||||
|
abstract fun getBackgroundColor(value: Int): Color
|
||||||
|
abstract fun getForegroundColor(value: Int): Color
|
||||||
|
}
|
||||||
|
|
||||||
|
fun playGame(game: Game, settings: GameSettings) {
|
||||||
|
with(JFrame()) {
|
||||||
|
title = settings.name
|
||||||
|
defaultCloseOperation = WindowConstants.EXIT_ON_CLOSE
|
||||||
|
setSize(340, 400)
|
||||||
|
isResizable = false
|
||||||
|
|
||||||
|
add(PlayGame(game, settings))
|
||||||
|
|
||||||
|
setLocationRelativeTo(null)
|
||||||
|
isVisible = true
|
||||||
|
}
|
||||||
|
}
|
25
src/assignment/week5/games/ui/PlayGame2048.kt
Normal file
25
src/assignment/week5/games/ui/PlayGame2048.kt
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
package games.ui
|
||||||
|
|
||||||
|
import games.game2048.newGame2048
|
||||||
|
import java.awt.Color
|
||||||
|
|
||||||
|
object Game2048Settings : GameSettings("Game 2048", Color(0xbbada0)) {
|
||||||
|
private val emptyColor = Color(0xcdc1b4)
|
||||||
|
private val colors: Map<Int, Color> = run {
|
||||||
|
val colors = listOf(
|
||||||
|
0xeee4da, 0xede0c8, 0xf2b179, 0xf59563, 0xf67c5f, 0xf65e3b,
|
||||||
|
0xedcf72, 0xedcc61, 0xedc850, 0xedc53f, 0xedc22e
|
||||||
|
)
|
||||||
|
|
||||||
|
val values: List<Int> = (1..11).map { Math.pow(2.0, it.toDouble()).toInt() }
|
||||||
|
values.zip(colors.map { Color(it) }).toMap()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getBackgroundColor(value: Int) = colors[value] ?: emptyColor
|
||||||
|
override fun getForegroundColor(value: Int) = if (value < 16) Color(0x776e65) else Color(0xf9f6f2)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
fun main() {
|
||||||
|
playGame(newGame2048(), Game2048Settings)
|
||||||
|
}
|
23
src/assignment/week5/games/ui/PlayGameOfFifteen.kt
Normal file
23
src/assignment/week5/games/ui/PlayGameOfFifteen.kt
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
package games.ui
|
||||||
|
|
||||||
|
import games.gameOfFifteen.newGameOfFifteen
|
||||||
|
import java.awt.Color
|
||||||
|
|
||||||
|
object GameOfFifteenSettings : GameSettings("Game of fifteen", Color(0x909090)) {
|
||||||
|
private val emptyColor = Color(0x787878)
|
||||||
|
private val firstColor = Color(0xC8C8C8)
|
||||||
|
private val secondColor = Color(0xCCCCFF)
|
||||||
|
private val foregroundColor = Color(0x545AA7)
|
||||||
|
|
||||||
|
override fun getBackgroundColor(value: Int) = when {
|
||||||
|
value == 0 -> emptyColor
|
||||||
|
((value - 1) / 4 + value % 4) % 2 == 0 -> firstColor
|
||||||
|
else -> secondColor
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getForegroundColor(value: Int) = foregroundColor
|
||||||
|
}
|
||||||
|
|
||||||
|
fun main() {
|
||||||
|
playGame(newGameOfFifteen(), GameOfFifteenSettings)
|
||||||
|
}
|
Reference in New Issue
Block a user