JavaScript for Python Programmers

A guided tour through the Dandelions game engine — real code, practical comparisons, no fluff.

Contents

  1. Foundations: how JS differs from Python
  2. createBoard
  3. copyBoard
  4. placeFlower
  5. blowWind
  6. checkWin
  7. getValidPlacements
  8. getUnusedDirections
  9. printBoard
  10. Quick reference table

Foundations

How JS runs in the browser

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 F12Console 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.

const, let, and var

JS has three ways to declare a variable. You should use two of them:

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.

JavaScript
const SIZE = 5;
let round = 0;
round = 1;      // ✓ let can reassign
SIZE = 6;       // ✗ TypeError
Python
SIZE = 5        # convention only
round = 0
round = 1       # fine
SIZE = 6        # also fine — no enforcement

2D arrays: JS vs numpy

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.

JavaScript
const board = [
  [0, 0, 0],
  [0, 1, 0],
  [0, 0, 0]
];
board[1][1];       // → 1
board[1][2] = 2;
Python
board = [
  [0, 0, 0],
  [0, 1, 0],
  [0, 0, 0]
]
board[1][1]        # → 1
board[1][2] = 2

Truthiness and falsiness

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.

Always use ===, never ==. Loose == does type coercion: 0 == false is true. Strict 0 === false is false. === behaves like Python's == — same value, same type.

The DIRECTIONS object — JS dict

The engine's DIRECTIONS constant is a plain JS object used as a dictionary. String keys, array values.

JavaScript
const DIRECTIONS = {
  N:  [-1,  0],
  NE: [-1,  1],
  // …
};
DIRECTIONS['N'];        // [-1, 0]
DIRECTIONS.N;           // same
Object.keys(DIRECTIONS);// ['N','NE',…]
Python
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'.

Try it — DIRECTIONS object

1 createBoard

Array.from() arrow functions Array.fill()
function createBoard() {
  return Array.from({ length: 5 }, () => Array(5).fill(0));
}

What it does

Creates and returns a fresh 5×5 board — a 2D array where every cell is 0 (empty).

The JS concepts

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.

Why not 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.
JavaScript
Array.from({ length: 5 },
  () => Array(5).fill(0)
);
Python
[[0] * 5 for _ in range(5)]
# NOT [[0]*5]*5 — same bug applies
Try it — createBoard

2 copyBoard

Array.map() spread operator shallow vs deep copy
function copyBoard(board) {
  return board.map(row => [...row]);
}

What it does

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.

The JS concepts

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).

JavaScript
board.map(row => [...row])

// spread merges arrays too:
[...a, ...b]  // like Python's [*a, *b]
Python
[row[:] for row in board]

[*a, *b]      # same merge trick
Try it — copyBoard

3 placeFlower

=== strict equality returning null immutable pattern
function placeFlower(board, row, col) {
  if (board[row][col] === 1) return null;
  const newBoard = copyBoard(board);
  newBoard[row][col] = 1;
  return newBoard;
}

What it does

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.

The JS concepts

=== 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.

JavaScript
if (board[row][col] === 1) return null;

const result = placeFlower(board, 2, 2);
if (result) { /* success */ }
Python
if board[row][col] == 1: return None

result = place_flower(board, 2, 2)
if result is not None: ...
Try it — placeFlower

4 blowWind

destructuring assignment while loop nested for loops let for mutation
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;
}

What it does

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.

The JS concepts

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 &&.

JavaScript
const [dr, dc] = DIRECTIONS[dir];
for (let r = 0; r < 5; r++) { ... }
// bounds:
nr >= 0 && nr < 5 && nc >= 0 && nc < 5
Python
dr, dc = DIRECTIONS[dir]
for r in range(5): ...
# bounds:
0 <= nr < 5 and 0 <= nc < 5
Try it — blowWind

5 checkWin

Array.every() chained array methods !== operator
function checkWin(board) {
  return board.every(row => row.every(cell => cell !== 0));
}

What it does

Returns true if no cell is empty. All cells must be a flower (1) or seed (2). One zero anywhere means a loss.

The JS concepts

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.

JavaScript
board.every(row =>
  row.every(cell => cell !== 0)
)
// any zero? use .some():
board.some(row => row.some(c => c === 0))
Python
all(cell != 0
    for row in board
    for cell in row)

any(cell == 0
    for row in board
    for cell in row)
every / some = all / any. Both JS pairs short-circuit.
Try it — checkWin

6 getValidPlacements

Array.push() object literals || OR operator
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;
}

What it does

Returns all cells where a flower can be placed — empty (0) or seeded (2). Existing flowers (1) are excluded.

The JS concepts

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.

JavaScript
placements.push({ row: r, col: c });
placements[0].row  // dot access
placements[0].col
Python
placements.append({'row': r, 'col': c})
placements[0]['row']
placements[0]['col']
Try it — getValidPlacements

7 getUnusedDirections

Object.keys() Array.filter() Array.includes() ! negation
function getUnusedDirections(usedDirections) {
  return Object.keys(DIRECTIONS).filter(d => !usedDirections.includes(d));
}

What it does

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.

The JS concepts

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."

JavaScript
Object.keys(DIRECTIONS)
  .filter(d => !usedDirections.includes(d))

[1,2,3].includes(2)  // true
Python
[d for d in DIRECTIONS
 if d not in used_directions]

2 in [1,2,3]         # True
Try it — getUnusedDirections

8 printBoard

object as lookup table for...of loop Array.map() + join() template literals console.log
function printBoard(board) {
  const symbols = { 0: '.', 1: '*', 2: '\u00b7' };
  for (const row of board) {
    console.log(row.map(cell => symbols[cell]).join(' '));
  }
}

What it does

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.

The JS concepts

{ 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 ${}.

JavaScript
for (const row of board) {
  console.log(row.map(c => sym[c]).join(' '));
}
console.log(`Round ${round} of 7`);
Python
for row in board:
    print(' '.join(sym[c] for c in row))
print(f'Round {round} of 7')
Template literals use backticks (`), not quotes. They also support true multi-line strings without escape characters — something Python f-strings don't do.
Try it — printBoard + template literals

Quick Reference

JavaScriptPythonNotes
=====No type coercion in JS
const / letassignmentconst ≈ 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 arrValue 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 arrIterate values
const [a, b] = arra, b = arrDestructuring / 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