Last active
October 29, 2023 14:20
-
-
Save scalway/fa05501167baf51ede02d2cff582ebc6 to your computer and use it in GitHub Desktop.
scala 3 experiments - modified version of code from https://www.reddit.com/r/scala/comments/17irfm5/rate_my_first_ever_scala_code/
This file contains hidden or 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
//> using scala 3 | |
//> using dep com.lihaoyi::fansi:0.4.0 | |
import scala.io.StdIn.readLine | |
import java.util.Arrays | |
case class Pos(val x: Int, val y: Int) | |
object Pos: | |
def fromUserInput(indexes:Seq[Int]): Option[Pos] = indexes match | |
case Seq(col, row) => Some(Pos(Math.floorMod(col-1, 3), Math.floorMod(row-1, 3))) | |
case other => None | |
class BoardPresentation(board:Board): | |
var player = Map(0 -> "_", 1 -> "O", 2 -> "X").withDefault(x => ""+x) | |
def showRows(highlited:Seq[Int]): Iterator[String] = | |
board.state.view.zipWithIndex.grouped(board.sizeCols).map: row => | |
row.map { case (x, idx) => player(x) }.mkString(" ") | |
def showLegend(forView:Iterator[String]): Iterator[String] = | |
val header = Iterator( | |
" | " + (1 to board.sizeCols).mkString(" "), | |
"--" * (board.sizeCols+2), | |
) | |
val rows = forView.zipWithIndex.map { case (row, idx) => s"${idx+1} | $row"} | |
header ++ rows | |
def show(highlighted:Seq[Int] = Seq.empty) = | |
println() | |
showLegend(showRows(highlighted)).foreach(println) | |
class ColoredBoardPresentation(board:Board) extends BoardPresentation(board): | |
import fansi.Color._ | |
player = | |
Map(0 -> White("_"), 1 -> Green("O"), 2 -> LightBlue("X")) | |
.withDefault(x => Red(""+x)).map { case (x, v) => x -> v.toString } | |
override def showRows(highlited:Seq[Int]): Iterator[String] = | |
board.state.view.zipWithIndex.map: | |
case (x, idx) => | |
var attr:fansi.Attrs = fansi.Bold.Off | |
if (board.lastMarked == idx) attr ++= fansi.Bold.On | |
if (highlited.contains(idx)) attr ++= fansi.Back.Cyan | |
attr(player(x)) | |
.grouped(board.sizeCols) | |
.map(_.mkString(" ")) | |
class Board(size:Int): | |
//sizeCols, sizeRows are not needed in square boards, but index calculation depends on one size only | |
//so I prefere to be specyfic here just in case we change rules. | |
val sizeCols, sizeRows = size | |
private val cellsCount = sizeCols*sizeRows | |
val state = Array.fill(cellsCount)(0) | |
var lastMarked = -1 | |
private def indexOf(colX:Int, rowY:Int):Int = rowY * sizeCols + colX | |
private def indexOf(p: Pos):Int = indexOf(p.x, p.y) | |
def getPos(p: Pos): Int = state(indexOf(p)) | |
def markPos(m: Int, p: Pos): Option[Int] = | |
val idx = indexOf(p) | |
val posWasEmpty = state(idx) == 0 | |
if posWasEmpty then | |
state(idx) = m | |
lastMarked = idx | |
Option.when(posWasEmpty)(idx) | |
def isFilled: Boolean = !state.contains(0) | |
object winCases: | |
val rows = (0 until sizeRows).map { row => indexOf(0, row) to indexOf(sizeCols-1, row) } //win rows | |
val cols = (0 until sizeCols).map { col => indexOf(col, 0) to indexOf(col, sizeRows-1) by sizeCols } //win cols | |
//Warn: If board is not square there will be more possible diagonals calculated differently! | |
val diag1 = (0 until size).map { x => indexOf(x, x) } | |
val diag2 = (0 until size).map { x => indexOf(sizeCols-x-1, x) } | |
val all = Seq(rows, cols, Seq(diag1, diag2)).flatten | |
private def checkIndexes(indexes:Seq[Int], player:Int) = | |
indexes.forall(i => state(i) == player) | |
def checkWin(currentPlayer: Int) = | |
winCases.all.find(checkIndexes(_, currentPlayer)) | |
end Board | |
object UserInput: | |
val TwoDigits = raw"(\d) ?(\d)".r | |
def getUserInput(prompt:String): Seq[Int] = readLine(prompt) match | |
case TwoDigits(col, row) => Seq(col.toInt, row.toInt) | |
case otherwise => Seq.empty | |
class Game: | |
private val board = Board(3) | |
private val view = ColoredBoardPresentation(board) | |
var playing = true | |
var currentPlayer = 1 | |
def nextPlayer() = currentPlayer = if currentPlayer == 1 then 2 else 1 | |
def end(comment:String) = | |
playing = false | |
println() | |
println("-" * comment.length()) | |
println(comment) | |
println("-" * comment.length()) | |
def start(): Unit = | |
while (playing) | |
view.show() | |
val userInput = UserInput.getUserInput(s"\n> player ${view.player(currentPlayer)}: ") | |
Pos.fromUserInput(userInput) match | |
case None => println("faulty input!") | |
case Some(pos) => | |
board.markPos(currentPlayer, pos) match | |
case None => println("position already marked!") | |
case Some(markedIdx) => | |
//todo: can be perf-optimised to check only those options that could be affected by last marked position | |
board.checkWin(currentPlayer) match | |
case Some(winRow) => | |
view.show(winRow) | |
end(s"player ${view.player(currentPlayer)} is the winner!") | |
case _ if board.isFilled => | |
end("nobody won!") | |
case _ => | |
nextPlayer() | |
end while | |
end start | |
end Game | |
@main def hello: Unit = | |
val game = Game() | |
game.start() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
run
or