Q-table Worked Example: A 4×4 Gridworld#

One fully worked computation is worth ten abstractions. Consider a 4×4 grid: start in the top-left, a goal with reward $+1$ in the bottom-right, reward $0$ elsewhere, actions {up, down, left, right}, $\gamma = 0.9$.

A 4×4 gridworld: start top-left, +1 goal bottom-right, with one 6-step example episode

The dashed path shows one possible episode: 6 steps to the goal, so its return is $G_0 = \gamma^5 \cdot 1 \approx 0.59$ — the single $+1$ reward, discounted once per step of delay. Value iteration below will confirm this is in fact the optimal value of the start state.

Value iteration by hand (first two sweeps)#

Initialize $V_0(s) = 0$ everywhere. Apply the Bellman optimality backup $V_{k+1}(s) = \max_a [R(s,a) + \gamma V_k(s')]$:

  • Sweep 1: only the cells adjacent to the goal get value: they can reach it in one step, so $V_1 = 1$ there (reward on entering the goal), $0$ elsewhere.
  • Sweep 2: cells two steps away become $\gamma \cdot 1 = 0.9$; the adjacent cells stay $1$.
  • Value “radiates” outward from the goal, discounted per step: $1,\ 0.9,\ 0.81,\ 0.729, \dots$

The greedy policy with respect to the converged $V^*$ simply walks downhill in distance-to-goal — as it should.

The same thing in code#

import numpy as np

N, gamma = 4, 0.9
V = np.zeros((N, N))
goal = (N - 1, N - 1)
moves = [(-1, 0), (1, 0), (0, -1), (0, 1)]

for sweep in range(50):
    V_new = np.zeros_like(V)
    for i in range(N):
        for j in range(N):
            if (i, j) == goal:
                continue
            best = -np.inf
            for di, dj in moves:
                ni = min(max(i + di, 0), N - 1)
                nj = min(max(j + dj, 0), N - 1)
                r = 1.0 if (ni, nj) == goal else 0.0
                best = max(best, r + gamma * V[ni, nj])
            V_new[i, j] = best
    V = V_new

print(np.round(V, 3))

Output — value radiating from the goal, $\gamma^{d-1}$ for a cell $d$ steps away:

[[0.59  0.656 0.729 0.81 ]     # start: 6 steps away ⇒ 0.9^5 ≈ 0.59
 [0.656 0.729 0.81  0.9  ]     # constant along anti-diagonals
 [0.729 0.81  0.9   1.   ]     # (equal distance to goal)
 [0.81  0.9   1.    0.   ]]    # goal itself: terminal, V = 0

Note two things worth pointing out: the start-state value $0.59$ matches the return of the 6-step episode in the figure above, and the goal cell stays $0$ — it is terminal, and the $+1$ is collected on entering it, not for being there.

Point of the demo: the entire solution came from repeatedly applying one local consistency rule. No gradient descent, no data set — just the Bellman operator. Deep RL replaces the table V[i, j] with a neural network; everything else is engineering around that substitution.