Skip to content

Instantly share code, notes, and snippets.

@scalway
Last active October 29, 2023 14:20
Show Gist options
  • Save scalway/fa05501167baf51ede02d2cff582ebc6 to your computer and use it in GitHub Desktop.
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/
//> 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()
@scalway
Copy link
Author

scalway commented Oct 29, 2023

run

$ scala-cli ./SmallGame.scala

or

$ scala-cli https://gist.github.com/scalway/fa05501167baf51ede02d2cff582ebc6

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment