Here's the repo so you can play for yourself!
This repository contains a version of Set implemented with linear logic
(I think. I don't know much about linear logic).
In this version of the code,
the GameCore
structure,
which handles the actual logic of the game
and makes sure that all moves are valid,
does not store the cards that are currently in play.
Instead, when the code starts a new game,
it recieves the GameCore
object and the 12 starting cards,
which it has to store itself.
When a player selects three cards,
the textui code passes those three cards to the game core,
and (sometimes) recieves three new cards back.
Using some clever compile-time stuff, we can ensure that a card is never used multiple times, and that cards from one game are never used in a different game.
The first part of that is relatively easy:
we just don't implement Clone
on the GameCard
s,
and the GameCore.move()
function takes ownership of the cards passed into it,
so playing a move requires giving up the card.
The second part is a bit harder: I used a trick from this post.
The basic idea
is to add do-nothing lifetime parameters to the GameCore
and GameCard
objects,
and then instead of exposing the new()
method on GameCore
,
we have a with_core
method that takes a closure which accepts a GameCore
as its argument.
Because of the for <'any>
trait bound on the closure,
the closure has to work for any possible lifetime of the GameCore
,
which means it can't use cards from one GameCore
with a different GameCore
.
The exact details of this are more complicated
and I refer interested readers to the post above for more clarification.
In line with that post, I too will recommend not doing this, except as a fun little experiment like here. But it does expose an interesting potential for complex compile-time checks in a borrow-checking system like Rust's.