function initializeRotaGame({
playWithComputer,
size = 640,
margin = 20,
strokeWidth = 2,
slotRadius = 30
} = {}) {
const rotaMachine = xs.createMachine(
{
...machineSpec,
context: {
playingWithComputer: playWithComputer,
player: undefined,
computersPiece: undefined,
pieceIndex: undefined,
positions: nextPositions(),
won: false
}
},
{
actions: {
resetPieces: xs.assign(({ context, event }) => {
return {
player: undefined,
pieceIndex: undefined,
positions: nextPositions(),
won: false
};
}),
setPlayer: xs.assign(({ context, event }) => {
const { playingWithComputer } = context;
const { piece } = event;
return {
player: piece,
computersPiece: piece === "a" ? "b" : "a"
};
}),
setNextPlayer: xs.assign(({ context }) => {
const { player } = context;
return {
player: player === "a" ? "b" : "a"
};
}),
setPiece: xs.assign(({ context, event }) => {
const { index } = event;
return {
pieceIndex: index
};
}),
movePiece: xs.assign(({ context, event }) => {
const { pieceIndex, positions } = context;
const { index } = event;
return {
positions: nextPositions(positions, pieceIndex, index)
};
}),
moveComputersPiece: xs.assign(({ context }) => {
const { positions, computersPiece } = context;
const [src, dest] = findComputersMove(positions, computersPiece);
return {
positions: nextPositions(positions, src, dest)
};
}),
setWinner: xs.assign(({ context }) => {
return {
player: findWinner(context.positions),
won: true
};
})
},
guards: {
checkWin: ({ context }) => findWinner(context.positions),
playersPiece: ({ context, event }) => {
const { player } = context;
const { piece } = event;
return piece === player;
},
isComputersTurn: ({ context }) => {
const { playingWithComputer, player, computersPiece } = context;
return playingWithComputer && player === computersPiece;
}
}
}
);
const actor = xs.createActor(rotaMachine);
const maxRadius = (size - 2 * margin - 2 * slotRadius) / 2;
const radius = maxRadius - strokeWidth / 2;
const root = d3
.create("svg")
.attr("width", size)
.attr("height", size)
.attr("viewBox", [-size / 2, -size / 2, size, size])
.attr("class", "rota-game-board");
// Symbols
const pieceRadius = slotRadius;
const pieceSymbols = root
.selectAll(".piece-symbol")
.data(["a", "b", "target"])
.join("symbol")
.attr("class", "piece-symbol")
.attr("id", (d) => `piece-${d}`)
.attr("viewBox", [
-pieceRadius,
-pieceRadius,
2 * pieceRadius,
2 * pieceRadius
]);
pieceSymbols.each(function (d) {
const el = d3.select(this);
let fill = palette[d] ?? "#ff0";
const fillAccent = d3.color(fill).brighter(1.25);
const bgRadius = pieceRadius - 2 * strokeWidth;
const bg = el.append("circle").attr("r", bgRadius).attr("fill", fill);
if (d === "a") {
el.append("circle")
.attr("r", bgRadius / 3)
.attr("fill", fillAccent);
} else if (d === "b") {
const rectSize = 0.8 * (2 / 3) * bgRadius;
el.append("rect")
.attr("width", rectSize)
.attr("height", rectSize)
.attr("x", -rectSize / 2)
.attr("y", -rectSize / 2)
.attr("fill", fillAccent)
.attr("transform", "rotate(45)");
} else if (d === "target") {
// bg.attr("r", 0.8 * bgRadius);
}
});
// Background
root
.append("rect")
.attr("width", size)
.attr("height", size)
.attr("x", -size / 2)
.attr("y", -size / 2)
.attr("fill", palette.bg);
const svg = root.append("g").attr("class", "canvas");
const board = svg.append("g").attr("class", "board");
const pieces = svg.append("g").attr("class", "pieces");
const legend = svg.append("g").attr("class", "legend");
const numOfOuterSlots = 8;
const slotAngles = d3
.range(numOfOuterSlots)
.map((d) => (2 * Math.PI * d) / numOfOuterSlots);
const lines = 4;
const lineAngles = d3.range(lines).map((d) => (Math.PI * d) / lines);
// Inner slot
board.append("circle").attr("r", slotRadius).attr("fill", palette.fg);
// Outline
board
.append("circle")
.attr("r", radius)
.attr("fill", "none")
.attr("stroke", palette.fg)
.attr("stroke-width", 3 * strokeWidth);
// Inner lines
board
.selectAll(".inner-line")
.data(lineAngles)
.join("line")
.attr("class", "inner-line")
.attr("x1", -radius)
.attr("x2", radius)
.attr("stroke", palette.fg)
.attr("stroke-width", strokeWidth * 3)
.attr("transform", (d) => `rotate(${radians2degree(d)})`);
const outerSlots = board
.selectAll(".outer-slot")
.data(slotAngles)
.join("g")
.attr("class", "outer-slot");
// Outer slots
outerSlots
.append("circle")
.attr("cx", (d) => radius * Math.cos(d))
.attr("cy", (d) => radius * Math.sin(d))
.attr("r", slotRadius)
.attr("fill", palette.fg);
// Outline - notch
board
.append("circle")
.attr("r", radius)
.attr("fill", "none")
.attr("stroke", palette.bg)
.attr("stroke-width", strokeWidth);
// Outer slots - notch
outerSlots
.append("circle")
.attr("cx", (d) => radius * Math.cos(d))
.attr("cy", (d) => radius * Math.sin(d))
.attr("r", slotRadius - strokeWidth)
.attr("fill", palette.bg);
board
.selectAll(".inner-line-notch")
.data(lineAngles)
.join("line")
.attr("class", "inner-line-notch")
.attr("x1", -radius)
.attr("x2", radius)
.attr("stroke", palette.bg)
.attr("stroke-width", strokeWidth)
.attr("transform", (d) => `rotate(${radians2degree(d)})`);
// Inner slot - notch
board
.append("circle")
.attr("r", slotRadius - strokeWidth)
.attr("fill", palette.bg);
// Legend
const legendPieceRadius = pieceRadius / 2;
const legendPieceX = -(maxRadius + 2 * legendPieceRadius);
const legendTextX = legendPieceX + legendPieceRadius * 2.25;
const legendTextXOnlyText = legendPieceX;
const legendPiece = legend
.append("use")
.attr("class", "piece")
.attr("width", 2 * legendPieceRadius)
.attr("height", 2 * legendPieceRadius)
.attr("x", legendPieceX)
.attr("y", maxRadius);
// .attr("href", `#piece-target`);
const legendText = legend
.append("text")
.attr("class", "legend-text")
.attr("x", legendTextX)
.attr("y", maxRadius + legendPieceRadius)
.attr("dominant-baseline", "central")
.attr("fill", palette.text)
.attr("font-size", "18px")
.attr("font-weight", "600");
// .text("hello");
function updateBoard(context, showTargets) {
const { pieceIndex, positions } = context;
const positionsFlat = (
showTargets ? addTargetPositions(positions, pieceIndex) : positions
).flat();
const angles = [
[(Math.PI / 4) * 5, (Math.PI / 4) * 6, (Math.PI / 4) * 7], // row 1
[(Math.PI / 4) * 4, undefined, 0], // row 2
[(Math.PI / 4) * 3, (Math.PI / 4) * 2, Math.PI / 4] // row 3
].flat();
pieces
.selectAll(".piece")
.data(angles)
.join("use")
.attr("class", "piece")
.attr("width", 2 * pieceRadius)
.attr("height", 2 * pieceRadius)
.each(function (angle, i) {
const piece = d3.select(this);
let x = 0;
let y = 0;
let type = positionsFlat[i];
if (angle !== undefined) {
x = radius * Math.cos(angle);
y = radius * Math.sin(angle);
}
piece
.attr("x", x - pieceRadius)
.attr("y", y - pieceRadius)
.attr("data-position-index", i)
.attr("data-piece-type", type)
.attr("href", type ? `#piece-${type}` : null);
});
}
function updateSVGAttributes(context) {
const { player, won } = context;
root.attr("data-player", player);
root.attr("data-won", won);
}
function updateLegend(state, context) {
const { player } = context;
if (state === "start") {
legendText.text("Pick a piece to start").attr("x", legendTextXOnlyText);
} else if (state === "pieces") {
legendText.text("Plays").attr("x", legendTextX);
} else if (state === "targets") {
legendText.text("Pick a position to move").attr("x", legendTextX);
} else if (state === "won") {
legendText.text("Wins!").attr("x", legendTextX);
}
if (state !== "start") {
legendText.attr("x", legendTextX);
}
legendPiece.attr("href", `#piece-${player}`);
}
actor.subscribe((s) => {
const { context, value } = s;
console.log(`Rota Machine: ${JSON.stringify(value)}`, s.context);
updateBoard(context, value === "targets");
updateSVGAttributes(context);
updateLegend(value, context);
});
actor.start();
root.on(
"click",
function (e) {
for (
var target = e.target;
target && target !== this;
target = target.parentNode
) {
if (target.matches(".pieces .piece")) {
const pIndex = target?.dataset.positionIndex;
const pType = target?.dataset.pieceType;
if (["a", "b"].includes(pType)) {
actor.send({ type: "PICK_PIECE", index: pIndex, piece: pType });
} else if (["target"].includes(pType)) {
actor.send({ type: "PICK_TARGET", index: pIndex });
}
// break;
return;
}
}
actor.send({ type: "CANCEL" });
},
false
);
return Object.assign(root.node(), { actor });
}