DEV Community

Tomasz Wegrzanowski
Tomasz Wegrzanowski

Posted on

Electron Adventures: Episode 87: Svelte Drag and Drop Chess Board

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})
    }
  }
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

Results

Here's the results, obviously ignoring the usual rules of chess:

Episode 87 Screenshot

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.

Discussion (0)