A guided tour through the Dandelions game engine — real code, practical comparisons, no fluff.
Python needs an interpreter — you run python script.py and it executes.
JavaScript is built into every browser. Write code inside an HTML file
(or link a .js file), open it, and it runs. No install, no terminal.
Open DevTools with F12 → Console to see output
and try one-liners interactively — it's the browser's REPL.
In Dandelions, index.html has <script src="game.js">.
The browser loads it top-to-bottom and everything defined at the top level lives
in the page's global scope.
JS has three ways to declare a variable. You should use two of them:
const — the binding can't be reassigned. Use this by default.let — the binding can be reassigned. Use when you need to update the variable.var — old, function-scoped, hoisted in surprising ways. Never use it in new code.
Neither const nor let has a direct Python equivalent —
Python just has assignment with no enforcement. Think of const as a
signal to yourself (and anyone reading) that this value won't change.
const SIZE = 5; let round = 0; round = 1; // ✓ let can reassign SIZE = 6; // ✗ TypeError
SIZE = 5 # convention only round = 0 round = 1 # fine SIZE = 6 # also fine — no enforcement
JS has no numpy. A "2D array" is just an array of arrays. Indexing is identical to Python plain lists. The differences show up in creation and copying — you'll see exactly how in the first two functions.
const board = [ [0, 0, 0], [0, 1, 0], [0, 0, 0] ]; board[1][1]; // → 1 board[1][2] = 2;
board = [ [0, 0, 0], [0, 1, 0], [0, 0, 0] ] board[1][1] # → 1 board[1][2] = 2
Both languages have truthy/falsy values, but the sets differ.
JS falsy: false, 0, "", null,
undefined, NaN. Notice 0 is falsy — which
matters in board logic since empty cells are 0.
===, never ==.
Loose == does type coercion: 0 == false is true.
Strict 0 === false is false. === behaves like
Python's == — same value, same type.
The engine's DIRECTIONS constant is a plain JS object used as a dictionary.
String keys, array values.
const DIRECTIONS = {
N: [-1, 0],
NE: [-1, 1],
// …
};
DIRECTIONS['N']; // [-1, 0]
DIRECTIONS.N; // same
Object.keys(DIRECTIONS);// ['N','NE',…]
DIRECTIONS = {
'N': (-1, 0),
'NE': (-1, 1),
# …
}
DIRECTIONS['N'] # (-1, 0)
# no dot access for dicts
list(DIRECTIONS.keys()) # ['N','NE',…]
Use dot notation (obj.key) when the key is a literal you know at write time.
Use bracket notation (obj[variable]) when the key is in a variable —
like DIRECTIONS[dir] where dir = 'NE'.
function createBoard() {
return Array.from({ length: 5 }, () => Array(5).fill(0));
}
Creates and returns a fresh 5×5 board — a 2D array where every cell is 0 (empty).
Array.from({ length: 5 }, callback) is the JS idiom for "make an array by
calling a function N times." The first argument is any object with a length
property; the second is a mapping function called once per index.
() => Array(5).fill(0) is an arrow function. No arguments
(()), returns a 5-element array of zeros. Python equivalent:
lambda: [0]*5, though you'd more naturally write it as a list comprehension.
Array(5).fill(Array(5).fill(0))?
fill with an object fills every slot with the same reference.
Modifying one row would modify all rows — the same gotcha as Python's
[[0]*5]*5. The arrow function creates a fresh inner array each time.
Array.from({ length: 5 },
() => Array(5).fill(0)
);
[[0] * 5 for _ in range(5)] # NOT [[0]*5]*5 — same bug applies
function copyBoard(board) {
return board.map(row => [...row]);
}
Returns a deep copy of the board. Changes to the copy don't affect the original —
essential because blowWind and placeFlower both return
a new board without mutating the input.
array.map(fn) applies a function to every element and returns a new array.
Python equivalent: [fn(x) for x in array]. JS's .map()
always returns a new array immediately (Python's map() returns a lazy iterator).
[...row] is the spread operator inside an array literal —
it expands the iterable into individual elements, producing a shallow copy of the row.
Since rows hold numbers (primitives), a shallow copy is a true copy. Python equivalent:
row[:] or list(row).
board.map(row => [...row]) // spread merges arrays too: [...a, ...b] // like Python's [*a, *b]
[row[:] for row in board] [*a, *b] # same merge trick
function placeFlower(board, row, col) {
if (board[row][col] === 1) return null;
const newBoard = copyBoard(board);
newBoard[row][col] = 1;
return newBoard;
}
Attempts to place a flower at (row, col). Valid on empty (0)
or seed (2) cells; rejected on an existing flower (1).
Returns the new board on success, null on failure.
=== is strict equality — value and type, no coercion. Always use this
instead of ==. Here it's identical to Python's ==.
null is JS's deliberate "no value," like Python's None.
Returning null for an invalid move lets the caller check
if (result) — null is falsy so it reads naturally.
The function never mutates its input — it calls copyBoard first, then
modifies the copy. This immutable data pattern makes the logic easy to
follow and trivial to undo, since you can always keep a "before" snapshot.
if (board[row][col] === 1) return null;
const result = placeFlower(board, 2, 2);
if (result) { /* success */ }
if board[row][col] == 1: return None result = place_flower(board, 2, 2) if result is not None: ...
function blowWind(board, direction) {
const [dr, dc] = DIRECTIONS[direction];
const newBoard = copyBoard(board);
for (let r = 0; r < 5; r++) {
for (let c = 0; c < 5; c++) {
if (board[r][c] === 1) {
let nr = r + dr;
let nc = c + dc;
while (nr >= 0 && nr < 5 && nc >= 0 && nc < 5) {
if (newBoard[nr][nc] === 0) {
newBoard[nr][nc] = 2;
}
nr += dr;
nc += dc;
}
}
}
}
return newBoard;
}
Simulates wind in one compass direction. For each flower on the board, traces outward
step-by-step in that direction, filling empty cells (0) with seeds
(2). Passes through occupied cells without stopping — a key rule.
const [dr, dc] = DIRECTIONS[direction] is destructuring assignment
— unpacks an array into named variables. Identical to Python's tuple unpacking
dr, dc = DIRECTIONS[direction].
Outer loops use let r / let c because those are reassigned
each iteration. Inner let nr, nc for the same reason. Rule of thumb:
use let whenever you'll do x += something or x = newValue.
The while-loop bounds check can't be written as a chained comparison like Python's
0 <= nr < 5. In JS, each bound is explicit with &&.
const [dr, dc] = DIRECTIONS[dir];
for (let r = 0; r < 5; r++) { ... }
// bounds:
nr >= 0 && nr < 5 && nc >= 0 && nc < 5
dr, dc = DIRECTIONS[dir] for r in range(5): ... # bounds: 0 <= nr < 5 and 0 <= nc < 5
function checkWin(board) {
return board.every(row => row.every(cell => cell !== 0));
}
Returns true if no cell is empty. All cells must be a flower (1)
or seed (2). One zero anywhere means a loss.
array.every(fn) returns true only if the function returns
truthy for every element. Short-circuits on the first failure. Python equivalent:
all().
The two chained .every() calls read as: "every row is such that every cell
is not zero." Clean and expressive.
board.every(row => row.every(cell => cell !== 0) ) // any zero? use .some(): board.some(row => row.some(c => c === 0))
all(cell != 0
for row in board
for cell in row)
any(cell == 0
for row in board
for cell in row)
function getValidPlacements(board) {
const placements = [];
for (let r = 0; r < 5; r++) {
for (let c = 0; c < 5; c++) {
if (board[r][c] === 0 || board[r][c] === 2) {
placements.push({ row: r, col: c });
}
}
}
return placements;
}
Returns all cells where a flower can be placed — empty (0) or seeded
(2). Existing flowers (1) are excluded.
array.push(value) appends in place, like Python's list.append().
It returns the new array length, which is rarely needed.
{ row: r, col: c } is an object literal — an inline dict.
Idiomatic JS for returning structured data. Python equivalent:
{'row': r, 'col': c}, or a namedtuple / dataclass.
placements.push({ row: r, col: c });
placements[0].row // dot access
placements[0].col
placements.append({'row': r, 'col': c})
placements[0]['row']
placements[0]['col']
function getUnusedDirections(usedDirections) {
return Object.keys(DIRECTIONS).filter(d => !usedDirections.includes(d));
}
Given a list of already-used direction strings, returns the ones not yet used. The game uses 7 of 8 compass directions, so this tells the wind which choices remain.
Object.keys(obj) returns an array of the object's own string keys, like
Python's list(d.keys()). Preserves insertion order in practice.
array.filter(fn) returns a new array of elements for which the function
returns truthy. Closest Python equivalent: [x for x in arr if condition(x)].
array.includes(value) is Python's value in list.
!usedDirections.includes(d) reads "d is not in usedDirections."
Object.keys(DIRECTIONS) .filter(d => !usedDirections.includes(d)) [1,2,3].includes(2) // true
[d for d in DIRECTIONS if d not in used_directions] 2 in [1,2,3] # True
function printBoard(board) {
const symbols = { 0: '.', 1: '*', 2: '\u00b7' };
for (const row of board) {
console.log(row.map(cell => symbols[cell]).join(' '));
}
}
Prints the board to the browser console.
. = empty, * = flower, · = seed.
Open DevTools (F12) to see output — useful for debugging at any point in the game.
{ 0: '.', 1: '*', 2: '·' } is a plain object used as a lookup table —
same as Python's {0: '.', 1: '*', 2: '·'} dict.
for (const row of board) iterates the values of an iterable, like Python's
for row in board. Use for...of when you don't need the index.
(When you do need the index, use for...of board.entries() or a classic
for (let i = 0; ...) loop.)
row.map(cell => symbols[cell]).join(' ') chains two methods:
.map() transforms each cell to a symbol, .join(sep)
concatenates into a string — identical to Python's ' '.join(...).
console.log is print. It accepts any number of comma-separated
arguments and prints them space-separated. Template literals let you embed expressions:
`Round ${round} of 7` — same idea as Python f-strings, but using backticks
and ${}.
for (const row of board) {
console.log(row.map(c => sym[c]).join(' '));
}
console.log(`Round ${round} of 7`);
for row in board:
print(' '.join(sym[c] for c in row))
print(f'Round {round} of 7')
`), not quotes.
They also support true multi-line strings without escape characters — something
Python f-strings don't do.
| JavaScript | Python | Notes |
|---|---|---|
=== | == | No type coercion in JS |
const / let | assignment | const ≈ re-assign guard |
Array.from({length:n}, fn) | [fn() for _ in range(n)] | Create array by factory |
[...arr] | arr[:] | Shallow copy via spread |
arr.map(fn) | [fn(x) for x in arr] | Returns new array |
arr.filter(fn) | [x for x in arr if fn(x)] | Returns new array |
arr.every(fn) | all(fn(x) for x in arr) | Short-circuits |
arr.some(fn) | any(fn(x) for x in arr) | Short-circuits |
arr.includes(x) | x in arr | Value membership |
arr.flat() | list(chain.from_iterable(arr)) | Flatten one level |
arr.join(sep) | sep.join(arr) | Strings only |
arr.push(x) | arr.append(x) | Mutates in place |
Object.keys(obj) | list(d.keys()) | Returns array of keys |
for (const x of arr) | for x in arr | Iterate values |
const [a, b] = arr | a, b = arr | Destructuring / unpacking |
`Hello ${name}` | f'Hello {name}' | Template literal / f-string |
console.log(x) | print(x) | Output (browser console) |
Dandelions game engine by mycarta · Game concept from Math Games with Bad Drawings by Ben Orlin