Public
Edited
Jan 7, 2024
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
gameloop = {
if(matches) {
let level = currentWord.game_level
mutable currentWord = picks[currentWord.n + 1]
if(level === 3) endGame()
}
yield true;
}
Insert cell
mutable currentWord = picks[0]
Insert cell
currentBoard = boardFromString(currentWord.boardstring)
Insert cell
mutable data = currentBoard
.map((x,i) => ({
...x,
id: i+1,
clicked: false,
clickable: x.letter === "_" ? false : true,
selected_order: 0, // maybe jsonstringify doesn't want nulls? but needed a 0 here
}))
Insert cell
mutable gameState = 'pre-game'
Insert cell
mutable startTime = new Date()
Insert cell
mutable stopTime = new Date()
Insert cell
viewof level = Inputs.input(1)
Insert cell
matches = guess === currentWord.word
Insert cell
guess = data
.filter(x=>x.selected_order > 0)
.sort((x,y) => x.selected_order - y.selected_order)
.map(x => x.letter)
.join('')
Insert cell
Insert cell
handleClickState = id => {
const output = handleClick(id, data)
mutable data = output
return output
}
Insert cell
handleClick = (id, data) => {
// make a deep copy of data
let copy = JSON.parse(JSON.stringify(data))

// get record for clicked tile
let tile = copy.filter(x => x.id === id)[0]
// check if clickable, if not do nothing
if(tile.clickable === false) return copy
// get max click_order
let max_click_order = Math.max(...data.map(x=> x.selected_order)) || 0
// get currently active tile
let lastTile = copy.filter(x => max_click_order > 0 && x.selected_order === max_click_order)[0]

// // check if record is neighbor
let recordIsNeighbor = lastTile === undefined || (Math.abs(lastTile.x - tile.x) <= 1 && Math.abs(lastTile.y - tile.y) <= 1)
// // the record is not a neighbor do nothing, but we need to pass if there was no lastTile to check against
if(max_click_order > 0 && recordIsNeighbor === false) return copy

// If the tile is a clickable neighbor, set row to clicked and click_order to max+1
tile.clickable = false
tile.clicked = true
tile.selected_order = max_click_order + 1
return copy
}
Insert cell
undoState = () => {
const output = undo(data)
mutable data = output
return output
}
Insert cell
undo = (data) => {
// make a deep copy of data
let copy = JSON.parse(JSON.stringify(data))
// get max click_order
let max_click_order = Math.max(...copy.map(x=> x.selected_order)) || 0
// get currently active tile
let lastTile = copy.filter(x => x.selected_order === max_click_order)[0]
lastTile.selected_order = 0
lastTile.clicked = false
lastTile.clickable = true

return copy
}
Insert cell
restart = () => {
mutable data = currentBoard
.map((x,i) => ({
...x,
id: i + 1,
clicked: false,
clickable: x.letter === "_" ? false : true,
selected_order: 0, // maybe jsonstringify doesn't want nulls? but needed a 0 here
}))
mutable gameState = 'in-game'
}
Insert cell
newPuzzle = (n) => {
if(typeof n === "number") {
mutable currentWord = picks[gameIndexToRow(n)]
}else{
mutable currentWord = _.sample(picks.filter(x=>x.game_level === 1), 1)
}
mutable gameState = 'in-game'
mutable startTime = new Date()
}
Insert cell
endGame = () => {
set(viewof level, level + 1)
mutable stopTime = new Date()
mutable gameState = 'post-game-win'
}
Insert cell
giveUp = () => {
// set(viewof level, level + 1)
mutable stopTime = new Date()
mutable gameState = 'post-game-lose'
}
Insert cell
Insert cell
picks = FileAttachment("output_csv.csv").csv({typed: true})
Insert cell
boardFromString = boardstring => boardstring
.split("")
.map((l,i) => ({
x: i % 4,
y: Math.floor(i/4.0),
letter: l
}))
Insert cell
difficulties = {
// take the first record from picks for each unique difficulty
let output = []
let seenDifficulties = []
for (const x of picks) {
if(seenDifficulties.includes(x.difficulty)) continue
output.push({...x, is_clicked: x.n === 0})
seenDifficulties.push(x.difficulty)
}
return output
}
Insert cell
difficultyToGameIndex = str => difficulties
.filter(x=>x.difficulty === str)[0].game_index
Insert cell
gameIndexToDifficulty = n => picks
.filter(x=> x.game_index === n)[0].difficulty
Insert cell
gameIndexToRow = n => picks
.filter(x=> x.game_index === n)[0].n
Insert cell
puzzleIndexMax = Math.max(...picks.map(x=> x.game_index)) - 1
Insert cell
Insert cell
Insert cell
scoreboard = () => html`<h3>Puzzle #${currentWord.game_index}</h3>
<svg width="100" height="30">
<circle cx="15" cy="15" r="10" stroke="black" stroke-width="1"
fill="${currentWord.game_level > 1 || gameState === 'post-game-win' ? 'black' : 'none'}" />
<circle cx="50" cy="15" r="10" stroke="black" stroke-width="1"
fill="${currentWord.game_level > 2 || gameState === 'post-game-win' ? 'black' : 'none'}" />
<circle cx="85" cy="15" r="10" stroke="black" stroke-width="1"
fill="${gameState === 'post-game-win' ? 'black' : 'none'}" />
</svg>
<h3>guess: ${guess}</h3>`
Insert cell
displayInGame = (data) => {
const max_selected = Math.max(...data.map(x=>x.selected_order))
const initDiv = x => {
let hitbox = html`<div class="hitbox"></div>`
e => handleClickState(x.id)
hitbox.onpointerdown = e => handleClickState(x.id)
hitbox.onpointerenter = (e) => {
if(e.buttons !== 0 && guess !== '') {
handleClickState(x.id)
}
}
let color = x.clicked === true ? '#479d2f' : 'black'
let backgroundColor = x.letter === '_' ? 'black' : 'white'
if(x.selected_order === max_selected && max_selected > 0) backgroundColor = '#bbdcac'
let div = html`<div
id=${x.id}
class="tile"
style="background-color:${backgroundColor}"
>
<div class="tileText" style="color:${color};">${x.letter}</div>
${hitbox}
</div>`
div.onpointerdown = e => handleClickState(x.id)
return div
}

const container = html`<div class="prevent-select"></div>`
container.appendChild(scoreboard())
const board = html`<div class="board"></div>`
const divs = data.map(x => initDiv(x))

for (const el of divs) board.appendChild(el)

const undoButton = html`<button>Undo</button>`
const restartButton = html`<button>Clear</button>`
const giveUpButton = html`<button>Reveal/Give Up</button>`
undoButton.onclick = undoState
restartButton.onclick = restart
giveUpButton.onclick = giveUp

container.appendChild(giveUpButton)
container.appendChild(restartButton)
container.appendChild(undoButton)

container.appendChild(board)
return container
}
Insert cell
displayPreGame = () => html`<h2>3 Big Words</h2>
<p>
<em>How to play:</em>
You will be shown a 4x4 grid of letters and black squares.
Using Boggle rules, use all the letters to make a big word.
Each letter is used exactly once. Touch each letter in order to find the word.
If you're right, a second grid will appear with a new word to find.
Find that word to see the third grid.
The goal is to find all three words as quickly as you can.
</p>
${Inputs.bind(Inputs.range([1,puzzleIndexMax], {step: 1, value: 1}), viewof level)}
${Inputs.bind(mapValue(radioButtons(difficulties, {
valueof: d => d.game_index,
format: d => d.difficulty,
value: 1,
}), {to: v => difficulties.findLast((d, i, arr) => d.game_index <= v).game_index}), viewof level)}
`
Insert cell
displayPostGameWin = () => html`<h2>You Did It!</h2>
<p>
You completed 3 Big Words Puzzle #${currentWord.game_index - 1} in ${Math.floor((stopTime - startTime) / 1000)} seconds!
<p>Play the next level, or select another puzzle:</p>
${Inputs.bind(Inputs.range([1,puzzleIndexMax], {step: 1, value: 1}), viewof level)}
${Inputs.bind(mapValue(radioButtons(difficulties, {
valueof: d => d.game_index,
format: d => d.difficulty,
value: 1,
}), {to: v => difficulties.findLast((d, i, arr) => d.game_index <= v).game_index}), viewof level)}
`
Insert cell
displayPostGameLose = () => html`<h2>Too Bad!</h2>
<p>
You gave up on puzzle #${currentWord.game_index}. The word you couldn't find was <b>${currentWord.word}</b>
<p>Play the next level, or select another puzzle:</p>
${Inputs.bind(Inputs.range([1,puzzleIndexMax], {step: 1, value: 1}), viewof level)}
${Inputs.bind(mapValue(radioButtons(difficulties, {
valueof: d => d.game_index,
format: d => d.difficulty,
value: 1,
}), {to: v => difficulties.findLast((d, i, arr) => d.game_index <= v).game_index}), viewof level)}
`
Insert cell
tileWidth = Math.min(width, 75)
Insert cell
<style>
button {
padding: 10px 15px;
font-size: 20px;
}
.board {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 0px;
width: ${4 * tileWidth}px;
touch-action: none;
}
.tile {
display: flex;
justify-content: center;
align-items: center;
height: ${tileWidth}px;
width: ${tileWidth}px;
font-family: sans-serif;
font-size: ${tileWidth * .83}px;
border-style: solid;
position: relative;
}
.tileText {
display: flex;
justify-content: center;
align-items: center;
width: 100%;
height: 100%;
user-select: none;
}
.hitbox {
position: absolute;
height: 70%;
width: 70%;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}

.prevent-select {
-webkit-user-select: none; /* Safari */
-ms-user-select: none; /* IE 10 and IE 11 */
user-select: none; /* Standard syntax */
}
</style>
Insert cell
Insert cell
function set(input, value) {
// https://observablehq.com/@observablehq/synchronized-inputs
input.value = value;
input.dispatchEvent(new Event("input", {bubbles: true}));
}
Insert cell
radioButtons = (values, {format: _format, ...options} = {}) => {
// Thanks to Fabian Iwand on the Observable slack #help channel https://observablehq.com/@mootari
const format = d => htl.html`<span>${_format ? _format(d) : d}`;
const form = Inputs.radio(values, {...options, format});
const scope = DOM.uid().id;
form.classList.add(scope);
form.append(htl.html`<style>
form.${scope} { max-width: initial }
.${scope} div { display: flex; flex-wrap: wrap; gap: 8px }
.${scope} div label { margin: 0; white-space: nowrap }
.${scope} input[type="radio"] { display: none }
.${scope} input[type="radio"] + span { border: 1px solid #aaa; background: #eee; padding: 4px }
.${scope} input[type="radio"] + span:hover { background: #ddd }
.${scope} input[type="radio"]:checked + span { background: #3b5fc0; color: white; text-decoration: underline }
`);
return form;
}
Insert cell
mapValue = (input, {from = v => v, to = v => v}) => Object.defineProperty(htl.html`<div>${input}`, "value", {
// Thanks to Fabian Iwand on the Observable slack #help channel https://observablehq.com/@mootari
get: () => from(input.value),
set: v => { input.value = to(v) },
})
Insert cell

One platform to build and deploy the best data apps

Experiment and prototype by building visualizations in live JavaScript notebooks. Collaborate with your team and decide which concepts to build out.
Use Observable Framework to build data apps locally. Use data loaders to build in any language or library, including Python, SQL, and R.
Seamlessly deploy to Observable. Test before you ship, use automatic deploy-on-commit, and ensure your projects are always up-to-date.
Learn more