#let position(..rows) = { rows.pos() } #let othello(pos, labels: true, cell-size: 30pt) = { let rows = pos // Colors let board-color = rgb("#2d8659") let line-color = rgb("#1a5c3a") let white-piece = rgb("#f0f0f0") let black-piece = rgb("#1a1a1a") let label-color = rgb("#000000") let flip-color = rgb("#d32f2f") let mask-color = rgb("#000000").transparentize(60%) // Calculate total size let board-size = cell-size * 8 let label-offset = if labels { 15pt } else { 0pt } let total-size = board-size + label-offset * 2 let piece-radius = cell-size * 0.4 let move-radius = cell-size * 0.25 box( width: total-size, height: total-size, { place( dx: label-offset, dy: label-offset, // Draw board background rect( width: board-size, height: board-size, fill: board-color, stroke: line-color + 2pt ) ) // Draw grid lines for i in range(1, 8) { let offset = cell-size * i // Vertical lines place( dx: label-offset + offset, dy: label-offset, line( start: (0pt, 0pt), end: (0pt, board-size), stroke: line-color + 1pt ) ) // Horizontal lines place( dx: label-offset, dy: label-offset + offset, line( start: (0pt, 0pt), end: (board-size, 0pt), stroke: line-color + 1pt ) ) } // Draw pieces and markers for (row-idx, row) in rows.enumerate() { for (col-idx, cell) in row.clusters().enumerate() { // Regular pieces if cell == "B" or cell == "W" { let x = label-offset + col-idx * cell-size + cell-size / 2 - piece-radius let y = label-offset + row-idx * cell-size + cell-size / 2 - piece-radius let piece-color = if cell == "B" { black-piece } else { white-piece } let piece-stroke = if cell == "W" { line-color + 0.5pt } else { none } place( dx: x, dy: y, circle( radius: piece-radius, fill: piece-color, stroke: piece-stroke ) ) } // Pieces to be flipped else if cell == "b" or cell == "w" { let x = label-offset + col-idx * cell-size + cell-size / 2 - piece-radius let y = label-offset + row-idx * cell-size + cell-size / 2 - piece-radius let piece-color = if cell == "b" { black-piece } else { white-piece } place( dx: x, dy: y, circle( radius: piece-radius, fill: piece-color, stroke: flip-color + 2pt ) ) } // Available moves else if cell == "0" or cell == "1" { let x = label-offset + col-idx * cell-size + cell-size / 2 - move-radius let y = label-offset + row-idx * cell-size + cell-size / 2 - move-radius let move-color = if cell == "0" { black-piece.transparentize(50%) } else { white-piece.transparentize(50%) } place( dx: x, dy: y, circle( radius: move-radius, fill: move-color, stroke: none ) ) } // Masked squares else if cell == "X" { let x = label-offset + col-idx * cell-size let y = label-offset + row-idx * cell-size place( dx: x, dy: y, rect( width: cell-size, height: cell-size, fill: mask-color, stroke: none ) ) } } } // Draw labels if labels { let files = ("a", "b", "c", "d", "e", "f", "g", "h") let ranks = ("8", "7", "6", "5", "4", "3", "2", "1").rev() // File labels (bottom) for (i, file) in files.enumerate() { let x = label-offset + i * cell-size + cell-size / 2 place( dx: x, dy: board-size + label-offset + 3pt, align(center, text(size: 10pt, fill: label-color, weight: "bold", file)) ) } // Rank labels (left) for (i, rank) in ranks.enumerate() { let y = label-offset + i * cell-size + cell-size / 2 place( dx: 3pt, dy: y, align(horizon, text(size: 10pt, fill: label-color, weight: "bold", rank)) ) } } } ) } #let othello-legend(cell-size: 20pt) = { let board-color = rgb("#2d8659") let line-color = rgb("#1a5c3a") let white-piece = rgb("#f0f0f0") let black-piece = rgb("#1a1a1a") let flip-color = rgb("#d32f2f") let mask-color = rgb("#000000").transparentize(60%) let piece-radius = cell-size * 0.4 let move-radius = cell-size * 0.25 let legend-item(symbol, description) = { grid( columns: (cell-size + 10pt, 1fr), align: (center + horizon, left + horizon), gutter: 10pt, // Symbol cell box( width: cell-size, height: cell-size, { place( rect( width: cell-size, height: cell-size, fill: board-color, stroke: line-color + 1pt ) ) symbol } ), // Description text(size: 10pt, description) ) } stack( dir: ttb, spacing: 6pt, legend-item( place( dx: cell-size / 2 - piece-radius, dy: cell-size / 2 - piece-radius, circle(radius: piece-radius, fill: black-piece) ), [Black disc] ), legend-item( place( dx: cell-size / 2 - piece-radius, dy: cell-size / 2 - piece-radius, circle(radius: piece-radius, fill: white-piece, stroke: line-color + 0.5pt) ), [White disc] ), legend-item( place( dx: cell-size / 2 - piece-radius, dy: cell-size / 2 - piece-radius, circle(radius: piece-radius, fill: black-piece, stroke: flip-color + 2pt) ), [Black disc to be flipped] ), legend-item( place( dx: cell-size / 2 - piece-radius, dy: cell-size / 2 - piece-radius, circle(radius: piece-radius, fill: white-piece, stroke: flip-color + 2pt) ), [White disc to be flipped] ), legend-item( place( dx: cell-size / 2 - move-radius, dy: cell-size / 2 - move-radius, circle(radius: move-radius, fill: black-piece.transparentize(50%)) ), [Available move for black] ), legend-item( place( dx: cell-size / 2 - move-radius, dy: cell-size / 2 - move-radius, circle(radius: move-radius, fill: white-piece.transparentize(50%)) ), [Available move for white] ), legend-item( place( rect(width: cell-size, height: cell-size, fill: mask-color) ), [Masked square] ), legend-item( [], [Empty square] ), ) } #let shift-indicator(values, cell-size: 30pt) = { assert(values.len() == 9, message: "shift-indicator requires exactly 9 values") let board-color = rgb("#2d8659") let line-color = rgb("#1a5c3a") let white-piece = rgb("#f0f0f0") let black-piece = rgb("#1a1a1a") let grid-size = cell-size * 3 let piece-radius = cell-size * 0.4 box( width: grid-size, height: grid-size, { // Draw background place( rect( width: grid-size, height: grid-size, fill: board-color, stroke: line-color + 2pt ) ) // Draw grid lines for i in range(1, 3) { let offset = cell-size * i // Vertical lines place( dx: offset, line( start: (0pt, 0pt), end: (0pt, grid-size), stroke: line-color + 1pt ) ) // Horizontal lines place( dy: offset, line( start: (0pt, 0pt), end: (grid-size, 0pt), stroke: line-color + 1pt ) ) } // Draw cells for row in range(3) { for col in range(3) { let idx = row * 3 + col let value = values.at(idx) let x = col * cell-size let y = row * cell-size if value == 0 { // Half-white, half-black disc let center-x = x + cell-size / 2 let center-y = y + cell-size / 2 // Draw full black circle place( dx: center-x - piece-radius, dy: center-y - piece-radius, circle(radius: piece-radius, fill: black-piece, stroke: none) ) // Draw white half-circle using clipping place( dx: center-x, dy: center-y - piece-radius, box( width: piece-radius, height: piece-radius * 2, clip: true, place( dx: -piece-radius, circle(radius: piece-radius, fill: white-piece, stroke: none) ) ) ) // Draw circle outline place( dx: center-x - piece-radius, dy: center-y - piece-radius, circle(radius: piece-radius, fill: none, stroke: line-color + 0.5pt) ) } else { // Arrow and number let arrow = if value > 0 { "«" } else { "»" } let num = str(calc.abs(value)) place( dx: x, dy: y, box( width: cell-size, height: cell-size, align( center + horizon, text(fill: white, weight: "bold", size: 10pt, arrow + " " + num) ) ) ) } } } } ) }