So I'm trying to write a dominoes game server, and I'm writing my core types, tiles and sets of dominoes and it occurs to me that including type information for the tile pips would allow me to write a much simpler function that creates chains of dominoes, I've started and left this project incomplete several times due to my being unable to figure this out, hoping someone has a simple type safe tile representation that leads to a simple domino chain function. the reason being is my current mental model is a board in a dominoes games is just an initial tile and 1-3 chains of dominoes each beginning and matching the pips on the initial tile.
Thanks so much in advance, and apologies for any imperfections in my question.
sealed case class DoubleSix[L >: Nat, R <: Nat](lPips: Int, rPips: Int) extends Tile[L, R]
object DoubleSixSet {
val zeroZero: DoubleSix[_0, _0] = DoubleSix(0, 0)
}
an older attempt at the type safe chaining function.
trait DominoChain[End] {
// val hasSpinner: Boolean
}
case object End extends DominoChain[Nothing] {
// val hasSpinner = false
}
case class Chain[D0, D1, X](head: Domino[D0, D1], tail: DominoChain[Domino[D1, X]]) extends DominoChain[Domino[D0, D1]] {
def hasSpinner =
if (head.isDouble || rest.hasSpinner) true
else false
}
As you probably noticed expressing dominoes in types is easy:
sealed trait One
sealed trait Two
sealed trait Three
sealed trait Four
sealed trait Five
sealed trait Six
sealed trait Domino[A, B] extends Product with Serializable
object Domino {
case object OneOne[One, One]
case object OneTwo[One, Two]
... // the other types of dominoes
}
If you want to have a linear chain it is also easy:
sealed trait Chain[A, B] extends Product with Serializable
object Chain {
case class One[A, B](domino: Domino[A, B]) extends Chain[A, B]
case class Prepend[A, B, C](head: Domino[A, B], tail: Chain[B, C]) extends Chain[A, C]
}
Things gets tricky if this is not linear though. You might want to make a turn. There is more than one way of doing this:
xxyy
yy
xx
xx
yy
xx
y
y
y
y
xx
and each of them would have to be expressed as a separate case. If you would like to avoid things like:
f <- f tile would have to be over or under bb tile
aabbc f
e c
edd
you would have to somehow detect such case and prevent it. You have 2 options here:
But in domino besides turns we can also have branches:
aab
bdd
cc
If you wanted to express it in type, now you have a two heads you can prepend to (and one tail you can append to). And during the game you could have more of them, so you would have to somehow express both: how many branches you have but also to which one do you want to add a new tile. Still possible, but but complicates your code even further.
You could e.g. express heads with some sort of HList (if you are using shapeless) and use that representation to provide implicit telling you which element of HList you want to modify.
However at this point you have very little benefits of type-level programming: you have to know you types ahead of time, you would have difficulties adding new tiles dynamically, you would have to persist state in such a way you would be able to retrieve the exact type so that type-level evidence would work...
Because of that I suggest an approach which is still type-safe while much easier to approach: just use smart constructors:
type Position = UUID
sealed trait Chain extends Product with Serializable
object Chain {
// prevent user from accessing constructors and copy directly
sealed abstract case class One private (
domino: Domino,
position: Position
) extends Chain
sealed abstract case class PrependLine private (
domino: Domino,
position: Position,
chain: Chain
)
sealed abstract case class Branch private (
chain1: Chain,
chain2: Chain
)
def start(domino: Domino): Chain
// check if you can add domino at this position, recursively rewrite tree
// if needed to add it at the right branch or maybe even create a new branch
def prepend(domino: Domino, to: Chain, at: Position): Either[Error, Chain]
}
This will still make it impossible to create an "invalid" domino chain. At the same time it will be much easier to add new rules, expand functionalities and persist state between requests (you mentioned that you want to build a server).