|
package slinkyIntro |
|
|
|
import slinky.core._ |
|
import slinky.core.annotations.react |
|
import slinky.core.facade.ReactElement |
|
import slinky.core.facade.Hooks.useState |
|
import slinky.web.html._ |
|
|
|
import scala.scalajs.js |
|
import scala.scalajs.js.annotation.{JSImport, ScalaJSDefined} |
|
|
|
@JSImport("resources/App.css", JSImport.Default) |
|
@js.native |
|
object AppCSS extends js.Object |
|
|
|
@JSImport("resources/logo.svg", JSImport.Default) |
|
@js.native |
|
object ReactLogo extends js.Object |
|
|
|
@react object Square { |
|
|
|
case class Props(value: String, isHighlighted: Boolean, onClick: () => Unit) |
|
|
|
val component: FunctionalComponent[Props] = FunctionalComponent[Props] { props => |
|
button( |
|
className := (if (props.isHighlighted) "highlighted " else "square"), |
|
onClick := props.onClick |
|
)({ |
|
props.value |
|
}) |
|
} |
|
} |
|
|
|
@react object Board { |
|
|
|
case class Props(squares: Array[String], highlights: Array[Boolean], onClick: Int => Unit) |
|
|
|
val component: FunctionalComponent[Props] = FunctionalComponent[Props] { props => |
|
|
|
def renderSquare(i: Int): ReactElement = Square(props.squares(i), isHighlighted = props.highlights(i), () => props.onClick(i)) |
|
|
|
val grid = for { |
|
y <- 0 until 3 |
|
} yield div(className := "board-row")(for { |
|
x <- 0 until 3 |
|
} yield renderSquare(x + y * 3)) |
|
|
|
div()(grid) |
|
} |
|
} |
|
|
|
object Game { |
|
|
|
def apply(): ReactElement = component() |
|
|
|
val component: FunctionalComponent[Unit] = FunctionalComponent[Unit] { _ => |
|
|
|
val (history, updateHistory) = useState(Array(Array.fill(9)(""))) |
|
val (stepNumber, updateStepNumber) = useState(0) |
|
val (isAscending, updateIsAscending) = useState(true) |
|
val (xIsNext, updateXIsNext) = useState(true) |
|
|
|
def calculateWinningLine(squares: Array[String]): Option[List[Int]] = { |
|
val lines: List[List[Int]] = List( |
|
List(0, 1, 2), |
|
List(3, 4, 5), |
|
List(6, 7, 8), |
|
List(0, 3, 6), |
|
List(1, 4, 7), |
|
List(2, 5, 8), |
|
List(0, 4, 8), |
|
List(2, 4, 6), |
|
) |
|
lines.find({ |
|
case a :: b :: c :: Nil => squares(a) != "" && squares(a) == squares(b) && squares(b) == squares(c) |
|
case _ => throw new Error |
|
}) |
|
} |
|
|
|
def getValue(condition: Boolean): String = if (condition) "X" else "O" |
|
|
|
def handleClick(i: Int): Unit = { |
|
|
|
val historyToStep = history.take(stepNumber + 1) |
|
val current = historyToStep.last |
|
val squares: Array[String] = Array(current: _*) |
|
|
|
if (squares(i) == "" && calculateWinningLine(squares).isEmpty) { |
|
squares(i) = getValue(xIsNext) |
|
updateHistory(historyToStep :+ squares) |
|
updateStepNumber(historyToStep.length) |
|
updateXIsNext(!xIsNext) |
|
} |
|
} |
|
|
|
def jumpTo(step: Int): Unit = { |
|
updateStepNumber(step) |
|
updateXIsNext((step % 2) == 0) |
|
} |
|
|
|
val current = history(stepNumber) |
|
|
|
val highlights = Array.fill(9)(false) |
|
|
|
val status = calculateWinningLine(current) match { |
|
case Some(line) => |
|
line.foreach(highlights(_) = true) |
|
"Winner: " + getValue(!xIsNext) |
|
case None if stepNumber < 9 => "Next player: " + getValue(xIsNext) |
|
case _ => "It's a draw" |
|
} |
|
|
|
def getMove(index: Int): Int = (0 until 9).find(m => history(index)(m) != history(index - 1)(m)).get |
|
|
|
def toCoords(move: Int): (Int, Int) = { |
|
val x = move % 3 |
|
(x, (move - x) / 3) |
|
} |
|
|
|
val moves = history.indices.map(index => { |
|
val desc = |
|
if (index == 0) "Go to game start" |
|
else "Go to move #" + index + " (" + toCoords(getMove(index)) + ")" |
|
li(key := index.toString)( |
|
button( |
|
className := (if (index == stepNumber) "bold-button" else ""), |
|
onClick := { _ => jumpTo(index) } |
|
)({ |
|
desc |
|
}) |
|
) |
|
}) |
|
|
|
val orderButton = button(onClick := { _ => |
|
updateIsAscending(!isAscending) |
|
})("Toggle order") |
|
|
|
div(className := "game")( |
|
div(className := "game-board")(Board(current, highlights, handleClick)), |
|
div(className := "game-info")( |
|
div(className := "status")(status), |
|
orderButton, |
|
ol(if (isAscending) moves else moves.reverse) |
|
) |
|
) |
|
} |
|
} |
|
|
|
object App { |
|
|
|
def apply(): ReactElement = component() |
|
|
|
private val css = AppCSS |
|
|
|
val component: FunctionalComponent[Unit] = FunctionalComponent[Unit] { _ => |
|
|
|
div(className := "App")( |
|
header(className := "App-header")( |
|
img(src := ReactLogo.asInstanceOf[String], className := "App-logo", alt := "logo"), |
|
h1(className := "App-title")("Welcome to React (with Scala.js!)") |
|
), |
|
p(className := "App-intro")( |
|
b("Slinky"), |
|
" version of the official ", |
|
a(href := "https://reactjs.org/tutorial/tutorial.html")( |
|
b("React"), |
|
"'s ", |
|
i("learn by doing"), |
|
" practical tutorial" |
|
), |
|
". To modify it, edit ", code("App.scala"), " and save to reload." |
|
), |
|
Game() |
|
) |
|
} |
|
} |