Chess Puzzles And Content Reveals For Daniel Caesar’s Never Enough

Are you ready to make a move?

Lee Martin
10 min readMar 27, 2023
Daniel Caesar contemplating his next move over a game of chess
Daniel Caesar

Reposted from my dev blog: leemartin.com/mate-in-one

When I first heard that some of the core thematic themes of Daniel Caesar’s upcoming album NEVER ENOUGH were chess and time, I thought to myself, “I’m going to have to build chess, aren’t I?” Well, through conversations with Republic Records and Daniel’s team, there was no shortage of ideas. In fact, we ended up considering a few concepts and I actually wrote two full fledged proposals (first was nuts by the way) but inevitably we ended up back at chess.

Chess, itself, isn’t exactly the most accessible of games. You either know how to play chess or you don’t. However, chess puzzles can reduce the requirement of a player down to a single move and it was through my initial research that I found “Mate in One.” A mate in one puzzle positions a chess board one move away from checkmate and the user is instructed to make the final move that would win them the game. You don’t need to have a complete understanding of playing chess, you merely need to understand how the pieces move and compromise the opponent’s king.

We decided to use this puzzle type as a means to deliver unlockable teasers leading up to the release of Daniel’s new album. Fans are invited to play a new game everyday to reveal a new piece of content. In addition, fans are also encouraged to share their score akin to Wordle. Are you ready to make a move? Play today’s game at checkmate.danielcaesar.com and read on to learn how this app was arranged.

Building Chess

Chess.js Engine

Early on in my development, I was floored to find the headless chess engine chess.js. This engine provides all the functionality of chess without the UI, making it a perfect candidate for the bespoke chess game we were building. Shout out to Jeff Hlywa and all of the other contributors that have made this engine available to developers. Chess.js provides all of the functions you’d expect to see from a chess engine. You can set up an initial arrangement of pieces, you can make moves, and you can listen for checkmate, among other functions.

To begin with, we want to arrange the board in the form of a chess puzzle. Chess.js allows you to provide this arrangement in the form of a Forsyth-Edwards Notation (FEN) string. This cleverly designed string tells you everything you need to know about which pieces exist on the board and where, who’s turn it is, and any other game critical details. Here’s a very simple mate in one puzzle’s FEN string being loaded into Chess.js.

const chess = new Chess('6k1/3R4/6K1/8/8/8/8/8 w KQkq - 0 1')

This would place the black king on the top row 8 in the 8th column g position. On row 7 a white (designated by uppercase) rook is in the d position. And, on row 6 a white king is in the g position. In order to win this game in a single move, the rook at d7 must move up to d8. Let's make that move using Chess.js.

chess.move({
from: 'd7',
to: 'd8'
})

This puts the black king into checkmate because a) it was put into check by the white rook which has challenged its position on row 8 and b) it cannot make another move without putting itself into check due to the position of both the white rook and the white king. We can check for checkmate on Chess.js using the following method.

chess.isCheckmate()

Beautiful, right? Thanks again to the Chess.js team for making this aspect of the app easy to implement.

Board Design

A ornamental chessboard designed in two colors: navy and cream
Chess board design

Chess.js doesn’t provide any UI for your chess implementation which I find incredibly inspiring as you can literally come up with any visual solution you can muster. This creates a rather fun UI/UX problem. For this app, I leaned on CSS grid for my board because it’s probably the most griddiest grid I’ve ever seen. First, we can establish an array of the squares.

let squares = [
"a8","b8","c8","d8","e8","f8","g8","h8",
"a7","b7","c7","d7","e7","f7","g7","h7",
"a6","b6","c6","d6","e6","f6","g6","h6",
"a5","b5","c5","d5","e5","f5","g5","h5",
"a4","b4","c4","d4","e4","f4","g4","h4",
"a3","b3","c3","d3","e3","f3","g3","h3",
"a2","b2","c2","d2","e2","f2","g2","h2",
"a1","b1","c1","d1","e1","f1","g1","h1",
]

Then, we can loop through each of these in Vue to create an element for each square.

<div id="chessboard">
<div class="square" data-square="square" v-for="square in squares"></div>
</div>

Finally, back in CSS, we can setup our 8x8 grid.

#chessboard{
display: grid;
grid-template-columns: repeat(8, minmax(0, 1fr));
grid-template-rows: repeat(8, minmax(0, 1fr));
}

Creating the final checkerboard pattern can be achieved in many ways. For example, you could style the squares themselves based on their nth-child() positioning or you could simple pass a responsive checkerboard pattern as the background of #chessboard. I ended up applying color to the squares themselves and gave them additional states such as .highlight for when a particular square was being targeted.

Pieces

I wanted to use standard familiar chess piece designs and found a great SVG set on Wikipedia of all places. Thanks to user Cburnett for his initial contribution to this set. I had to evolve the set a bit to add a stroke around the pieces because Daniel’s board is two color and there is a color blend issue. In my implemention, pieces are simply placed within the .square they belong.

<div class="square" data-square="square">
<div class="piece">
<img src="/images/wr.png" alt="white rook" />
</div>
</div>

My initial solution for movement was pretty simple, when a piece is clicked we store the square it is on as the moveFrom square. At this point, all the squares become clickable as moveTo squares. Now, I didn't mention it earlier but the Chess.js move() function will also throw an expection if an illegal move was made. If an illegal move happens, if a user doesn't checkmate the opponent, or if a user does checkmate the opponent, the user is notified via a custom dialog element. I loved having a good reason to implement this fancy new element.

Now, I thought the click to move solution would suffice but it didn’t take long during testing to realize that everyone… wanted to drag those pieces.

Drag and Drop

Ah, yes. Dragging and dropping. I was actually excited to add this bit of functionality because I was curious to take a closer look at the Drag and Drop API. However, this ended up being a hassle for me to integrate due to how that API is implemented and it’s spotty support on mobile. After a unproductive session with this API, I switched back to my tried and true drag and drop solution: Hammer.js

It’s amazing how well this library (which has not been updated in years) still handles the UX of draggable elements in most environments. To implement this, I converted my .piece element into a <Piece> Vue.js component so I could implement the functionality of dragging when the component is mounted. First, we initialize Hammer.js powered panning on the element.

// Create new instance of hammer on piece
let mc = new Hammer(el)

// Enable panning in all directions with 0 threshold
mc.add(new Hammer.Pan({
direction: Hammer.DIRECTION_ALL,
threshold: 0
}))

You’ll also want to keep track of whether the element is currently being dragged and what its initial position is.

// Position variables
let lastPosX = 0
let lastPosY = 0

// Dragging boolean
let isDragging = false

We can then listen for the pan event of Hammer.js. If the element is not being dragged, let's set isDragging to true and store the initial position. At this moment, we should also store the square it is sitting on as a moveFrom square, similar to the click event. Then, when the final pan event fires aka when a drop occurs, we'll check if the piece is currently over a .square. If so, we'll send that through to the Chess.js move function as the moveTo square. There is also a little bit of logic in here which handles the repositioning of the piece during drag.

// On pan
mc.on('pan', (e) => {
// Get target element
let dragElement = e.target

// If not dragging
if (!isDragging) {
// Start dragging
isDragging = true

// Store last position
lastPosX = dragElement.offsetLeft
lastPosY = dragElement.offsetTop

// Update moveFrom
moveFrom = dragElement.dataset.square

}

// Initialize new positions using delta change
let posX = e.deltaX + lastPosX
let posY = e.deltaY + lastPosY

// Update element position
dragElement.style.left = `${posX}px`
dragElement.style.top = `${posY}px`

// Get any drop elements
let dropElement = document.elementFromPoint(e.center.x, e.center.y)

// If this is the final drag
if (e.isFinal) {
// Stop dragging
isDragging = false

// If drop element is a square
if (dropElement.classList.contains('square')) {
// Update moveTo
moveTo = dropElement.dataset.square

// Move
chess.move({
from: moveFrom,
to: moveTo
})

} else {
// Reset position
dragElement.style.left = lastPosX
dragElement.style.top = lastPosY

}
}
})

By simplifying the solution here, we can end up sharing a lot of code between the click and drag/drop techniques and allow the user to perform either action.

Handling Scores

Statistics image example

When a user finally checkmates the opponents, they are able to share their score. I decided your score should be a combination of the time and how many attempts it took to achieve checkmate. Using the useClipboard component from VueUse, we allow users to copy their score to their device’s clipboard for easy pasting into a social post.

It took me 1 move and 00:00:02 to mate in one. ♟️ https://checkmate.danielcaesar.com

This technique was inspired by Wordle and the Wordle inspiration doesn’t stop there… I was also extremely interested in the original Wordle’s approach to using LocalStorage to track a user’s game stats over time. Subha Chanda wrote this great article about this topic and it inspired the solution I landed on. On our app, users can click the “stats” icon on the final page to see three lifetime stats on their playing.

  1. Games played — how many total games they have played
  2. Average moves — the average amount of moves it takes them to checkmate
  3. Average time — the average amount of time it takes them to checkmate

When users choose to share these statistics, the app generates a story formatted shareable image with their stats and a advertisement for the app itself. In addition to sharing today’s score, this allows users to share their overall performance and attract further users to the experience. Anyway, back to LocalStorage.

Back on VueUse, I found this excellent useStorage function which seamlessly allows you to add, edit, and delete data from the browser’s local storage. So, right when the user hits our app, we populate their storage with the current game state and these long term statistics.

// Use local storage state
const state = useStorage('never-enough', {
game: {
id: game.id,
fen: game.fen,
moves: 0,
status: "UNSTARTED",
timestamps: {
gameStarted: null,
lastMoved: null,
gameCompleted: null
}
},
stats: {
averageMoves: 0,
averageTime: 0,
gamesPlayed: 0
},
timestamp: Date.now()
})

As the game logic plays out, we can update the state. For example, when the game starts, we update the status:

// Update state state to playing
state.value.game.status = "PLAYING"

Or when a move is made, we increment the moves:

// Increment game moves
state.value.game.moves += 1

And when the user achieves checkmate, we can update the long term statistics accordingly. I just love this technique of tracking a user’s progress without requiring them to authenticate. The data is stored right in their browser and doesn’t require us to manage a database. I believe the accessibility of this approach was one of the factors that led to Wordle’s incredible success.

Managing Content

All of the content of our site is managed by the client in Contentful. This includes a list of mate in one chess puzzles with their associated FEN string and all of the media users can uncover. Using the scheduled publishing functionality of Contentful, we can prevent both the puzzles and media from being publicly available until it is absolutely necessary. Here’s the simple Nuxt 3 plugin I use to call Contentful from the app.

import * as contentful from 'contentful'

export default defineNuxtPlugin(nuxtApp => {
// Use runtime config
const runtimeConfig = useRuntimeConfig()

// Initialize config
const config = {
space: runtimeConfig.contentfulSpaceId,
accessToken: runtimeConfig.contentfulAccessToken,
host: runtimeConfig.contentfulHost
}

// Provide contentful client as helper
return {
provide: {
contentful: contentful.createClient(config)
}
}
})

Translating Experience

While researching Daniel’s socials and fanbase for this project, I noticed he had a large and active following in Brazil. For this reason, I suggested we translate the app to Portuguese for those fans who find themselves on our app. Integrating the i8ln module makes this work pretty easy. First, in our nuxt.config.js file, we augment the i8ln config options to include the Brazilian Portuguese locale. Side note: I was having a bit trouble with the browser detection functionality but setting useCookie to false seemed to do the trick for me.

i18n: {
detectBrowserLanguage: {
useCookie: false
},
defaultLocale: 'en',
locales: [
{
code: 'en'
}, {
code: 'pt'
}
]
}

Now, back in our Nuxt pages and components, we can use the per-component translation functionality of i8ln to declare the various translations required. I like the simplicity of this yaml config:

<i18n lang="yaml">
en:
query: 'Are you ready to make a move?'
pt:
query: 'Você está pronto para fazer um movimento?'
</i18n>

We must then configure the useI8ln module to use the local translations.

<script setup>
const { t } = useI18n({
useScope: 'local'
})
</script>

Finally, in the layout, we can use the t helper to display our translated text.

<template>
<p>{{ t('query') }}</p>
</template>

You can test locale changes using Chrome dev tools.

Thanks

It is always a great pleasure working with Allegra, Roderick, and their team at Republic Records. Special shout out to all of the creative folks Daniel surrounds himself with, who were very susceptible to innovative marketing concepts for this roll out. With two tracks now released, it is becoming clear that Daniel Caesar is about to launch a masterpiece. NEVER ENOUGH releases on 4.7. Until then, might I recommend streaming “Let Me Go” and playing some chess?

--

--

Netmaker. Playing the Internet in your favorite band for two decades. Previously Silva Artist Management, SoundCloud, and Songkick.