Now that we can roll the dice on a screen (well, spin a wheel) without making the cat chase them like physical dice, we'd also like a game board.
The goal is not to implement full game with all the game logic, just enough interactions to let players play. And that basically means drag and drop for the game pieces.
Browsers supported drag and drog for very long time, but it's fairly boilerplate-heavy code. So before we write our own, let's see how Svelte ecosystem looks like, and give svelte-dnd-action a try.
To make things interesting let's make a chess board. It won't know any rules of chess, except for the initial starting position of pieces. You can drag them in any way you want.
Grid structure
The layout of the app will be CSS grid. There will obviously be 8 columns. But there will be 10 rows. 8 regular rows, spacer row with nothing in it, and one big extra with big field to place killed pieces in.
initBoard
Let's start with initBoard
function, as it does a lot of things.
function initBoard() {
let pieces = [
"♜", "♞", "♝", "♛", "♚", "♝", "♞", "♜",
"♟︎", "♟︎", "♟︎", "♟︎", "♟︎", "♟︎", "♟︎", "♟︎",
"", "", "", "", "", "", "", "",
"", "", "", "", "", "", "", "",
"", "", "", "", "", "", "", "",
"", "", "", "", "", "", "", "",
"♙", "♙", "♙", "♙", "♙", "♙", "♙", "♙",
"♖", "♘", "♗", "♕", "♔", "♗", "♘", "♖",
"",
]
board = []
let items, cls
for (let i=0; i<65; i++) {
if (pieces[i] === "") {
items = []
} else {
items = [{id: i, symbol: pieces[i]}]
}
if (i === 64) {
cls = "offboard"
} else if ((i&1) ^ (Math.floor(i/8) & 1)) {
cls = "light"
} else {
cls = "dark"
}
board.push({items, cls})
}
}
Each field is represented by an object with two fields - items
(list of pieces it contains) and cls
(CSS class).
initBoard
needs to place the right chess pieces in the right places. To make drag and drop work, each piece must get a globally unique ID - we can just use i
for that.
We also need to assign how each field looks like. Half will be one color, half will be the other color, and the final field will be offboard
for pieces removed from the board.
There probably exists a simpler expression for choosing light/dark, this is a fun challenge if you'd like to play with that.
src/App.svelte
<script>
import Field from "./Field.svelte"
let board
initBoard()
</script>
<div class="board">
{#each board as field, i}
<Field {...field} />
{/each}
</div>
<style>
:global(body) {
background-color: #aaa;
color: #000;
text-align: center;
margin: 0;
min-height: 100vh;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
user-select: none;
}
.board {
display: grid;
grid-template-columns: repeat(8, 100px);
grid-template-rows: repeat(8, 100px) 50px 200px;
}
</style>
Now that we know how to initialize the board, App
component is just some simple styling.
src/Field.svelte
As logic is the same for regular fields and the offboard field for pieces removed from the board, I structured this component to support both roles, otherwise there would be a lot of duplication.
<script>
import Piece from "./Piece.svelte"
import {dndzone} from "svelte-dnd-action"
import {flip} from 'svelte/animate'
export let cls
export let items = []
function handleDND(e) {
items = e.detail.items
}
</script>
<div class="field {cls}" use:dndzone={{items}} on:consider={handleDND} on:finalize={handleDND}>
{#each items as item (item.id)}
<div animate:flip>
<Piece symbol={item.symbol} />
</div>
{/each}
</div>
<style>
.field {
border: 2px solid green;
margin: 0px;
background-color: #aaf;
display: flex;
align-items: center;
justify-content: center;
}
.dark {
background-color: #afa;
}
.offboard {
grid-column: 1 / span 8;
grid-row: 10;
}
</style>
There are a few interesting things here.
class="field {cls}"
lets initBoard
function outside control class of each component.
There's extra <div animate:flip>
that really looks like it should go inside Piece
but unfortunately that's not how Svelte animations work - they need to be directly under keyed #each
block in the same component. And we absolutely need those animations or drag and drop will have terrible jumps when pieces are moved around.
For drag and drop we need to pass a few things. use:dndzone={{items}}
sets up drag and drop and tells it to store contents in items
. We also set up handleDND
as handler for both drop preview and final drop. As we don't have any fancy logic, that's enough.
src/Piece.svelte
And finally Piece
component, basically just some styling. It looks like it wants <div animate:flip>
, but unfortunately that doesn't work, and we need to keep it outside.
<script>
export let symbol
</script>
<div>
{symbol}
</div>
<style>
div {
margin: 2px;
height: 36px;
width: 36px;
font-size: 36px;
line-height: 36px;
}
</style>
Results
Here's the results, obviously ignoring the usual rules of chess:
The svelte-dnd-action
library worked great, at least for this simple case.
With roulette wheel for dice, a board, and drag-and-droppable pieces, it's possible to make a lot of fun cat-proof board games. But let's set this aside for now, and for the next episode start another mini-project.
As usual, all the code for the episode is here.
Top comments (0)