My original assignment PDF (with all the handwritten Section 1–4 work) is linked above — the section headings below match its numbering exactly, so you can follow along page by page if you want to see my actual handwriting next to the typed-up version.
This report was generated using a custom Claude Code skill built by Dr. Teresa Vasquez. The skill takes the assignment PDF and a PDF of my own worked-out solutions — including notes on my reasoning, where I struggled, and what I learned — and digitizes that existing work into this formatted report, and (for this assignment specifically) into the blog-post format the professor asked for. AI is not solving the problems; it is transcribing, formatting, and turning into prose the reasoning and code I already worked through by hand and in R. The original uploaded files (assignment PDF, handwritten work PDF, and chat transcript) are included in the submission ZIP, which is available to professors.
Picture a guy leaving a bar, trying to walk home. He’s had a few, so he’s not walking in a straight line — every “step” he takes gets decided by flipping coins. Specifically: he flips two coins (coin 1 and coin 2) to decide his east-west movement, and one more coin (coin 3) to decide his north-south movement. Two heads on coins 1 and 2 sends him one step east, two tails sends him one step west, and a mixed result (one head, one tail) means he just sways in place and stays put. Coin 3 is simpler: heads sends him north, tails sends him south.
His home is 10 steps east and 5 steps south of the bar. I set a cap of 5,000 flips — if he hasn’t made it home by then, I count that walk as a failure and he’s presumably passed out on someone’s lawn.
The question I’m actually answering: on average, how many flips does it take him to get home — counting only the times he actually makes it? And then: what happens to that number if I rig one or two of the coins?
I answered this by hand first (working through the actual probabilities), then built an R simulation to check myself. That order matters — it’s the same order I’ll walk you through below, because the hand math is what let me catch a real bug in my own code later.
(Matches Section 1, items 1.1–1.7 on my worksheet — the PDF linked at the top has my actual handwriting for this part.)
Before assigning any probabilities, I had to convince myself of something that seems obvious but actually needs a reason: are all four outcomes of flipping coin 1 and coin 2 together equally likely?
| Coin 1 | Coin 2 | Result | Direction |
|---|---|---|---|
| H | H | Both heads | East |
| H | T | Mixed | Stay |
| T | H | Mixed | Stay |
| T | T | Both tails | West |
My first instinct was that with two coins the probability just gets “halved.” That’s not actually the mechanism. Each coin, on its own, is 50/50. What makes HH a specific probability is that the two flips are independent — coin 1’s result has zero effect on coin 2’s — and for independent events, \(P(A \text{ and } B) = P(A) \cdot P(B)\). Fairness alone doesn’t get you to “all four outcomes equally likely”; you need fairness and independence together, and that’s what justifies treating this like a simple 4-outcome sample space.
# Baseline case: every coin is fair (Pr(heads) = 0.5).
p_heads <- 0.5
# P(East): both coin 1 AND coin 2 land heads. Independent events, so
# multiply the two individual probabilities.
p_east <- p_heads * p_heads # .25
# P(West): both coin 1 AND coin 2 land tails.
p_west <- (1 - p_heads) * (1 - p_heads) # .25
# P(Stay): exactly one head and one tail, in EITHER order -- so this is
# P(H,T) + P(T,H), not just one of them.
p_stay <- (p_heads * (1 - p_heads)) + ((1 - p_heads) * p_heads) # .50
# Sanity check -- these three outcomes are the only ways coins 1 & 2 can
# land, so they have to add to 1.
p_east + p_west + p_stay
## [1] 1
# Coin 3 stands alone -- north/south never depends on coins 1 or 2.
p_north <- 0.5
p_south <- 0.5
I defined \(\Delta X\) as east-west movement in one flip (\(+1\) east, \(-1\) west, \(0\) stay) and \(\Delta Y\) as south-north movement (\(+1\) south, since south is the direction toward home, \(-1\) north). The expected value of each is just each possible value times how often it happens:
\[E[\Delta X] = (1)\cdot P(East) + (-1)\cdot P(West) + (0)\cdot P(Stay)\] \[E[\Delta Y] = (1)\cdot P(South) + (-1)\cdot P(North)\]
e_delta_x <- (1 * p_east) + (-1 * p_west) + (0 * p_stay)
e_delta_y <- (1 * p_south) + (-1 * p_north)
e_delta_x
## [1] 0
e_delta_y
## [1] 0
Both come out to 0. Zero. Here’s what that actually means, in my own words: because \(E[\Delta X] = E[\Delta Y] = 0\), any progress towards home is random fluctuation — the variance around the zero average. Because there is no push, it will take some time for the drunkard to get home. He’s not drifting toward his house at all; he only gets there because random noise happens to add up in the right direction eventually. That’s the whole reason a simulation is worth building instead of just doing more algebra: nobody can hand-derive “how long does pure noise take to wander 10 and 5 steps in a particular direction,” but a computer can just try it 5,000 times and count.
(The assignment hands you a code skeleton with steps 1 through 4 already labeled in comments, on page 1 before the worksheet sections even start. That’s the numbering I’m following in this section — “Step 1,” “Steps 2.1 / 2.2,” “Step 3,” “Step 4” — so it’s a different numbering track from “Section 1–4” above and below. This is the “breaking it a few times” part.)
I built this in pieces, and I want to walk through it the way it actually happened, bugs included, because the bugs are where I actually learned how the thing worked.
(Step 1 in the skeleton is just setting the seed and m,
which I do once, right before the simulations run further down the page
— see “Scaling Up to 5,000,” below.)
First I needed a function that flips two coins and returns a direction — I went with numbers (+1 / -1 / 0) rather than words like “east,” because numbers can just be added directly onto a running position.
# Simulates coins 1 & 2 together and returns the east-west step: +1 (east)
# if both land heads, -1 (west) if both land tails, 0 (stay) otherwise.
# p1 and p2 are passed in separately so the SAME function works for the
# fair case and both biased scenarios below -- I never rewrite this function.
flip_coin_x <- function(p1, p2) {
flip1 <- rbinom(1, 1, p1)
flip2 <- rbinom(1, 1, p2)
if (flip1 == 1 && flip2 == 1) {
return(1) # both heads -> east
}
if (flip1 == 0 && flip2 == 0) {
return(-1) # both tails -> west
}
0 # one head, one tail -> stay
}
# Simulates coin 3 and returns the north-south step. The problem says
# heads moves him NORTH -- and since +1 south / -1 north is my sign
# convention (south is toward home), heads has to return -1, not +1.
flip_coin_y <- function(p3 = .5) {
flip <- rbinom(1, 1, p3)
if (flip == 1) {
return(-1) # heads -> north
}
1 # tails -> south
}
Two mistakes I actually made here, worth naming because they’re the kind of bug that’s invisible until you trace through what the code should do:
if statements with no return() — so every
single call just fell through to whatever the last line was, no matter
what the coins actually did. Adding explicit return() calls
fixed it.flip_coin_y returned +1 for
heads. That’s backwards under my own sign convention — I had the
north/south signs flipped and had to go back and correct them.This is where the real debugging happened. My first attempt at tracking position looked like this:
loc_x <- c(flip_coin_x(p1, p2)) # WRONG
Running it, the drunkard’s X and Y values never left
{-1, 0, 1} across all 5,000 flips. My first thought was
that he’d just never gotten lucky. But I’d already worked out, by hand,
that a genuine zero-drift walk should wander much further than \(\pm 1\) over thousands of steps — that’s
exactly what Section 1.7 above predicted. So this wasn’t bad luck, it
was a bug: that line replaces the position every time instead
of accumulating it. Once I fixed it to
loc_x <- loc_x + flip_coin_x(p1, p2), positions started
reaching the tens, like they should.
I also had to get the loop’s stopping condition right. My first
attempt used || between “is he home” and “has he hit the
flip cap,” which never stopped. My second attempt used
&& between the X-check and the Y-check, which
stopped too early — the moment either coordinate happened to
land on target, even if the other one wasn’t. The correct reading of
“he’s not home yet” is that at least one coordinate is still
off, which is an || between the coordinate checks:
# Simulates one full walk: flip the coins repeatedly until the drunkard is
# at (10, 5) or we hit the 5,000-flip cap, whichever comes first.
compute <- function(p1, p2, p3) {
max_steps <- 5000
count <- 0
# Pre-allocate the full-length vectors up front (faster than growing
# them one element at a time) and trim off the unused tail afterward.
loc_x <- numeric(max_steps + 1)
loc_y <- numeric(max_steps + 1)
# Keep flipping as long as he is NOT home (X off target OR Y off
# target -- either one being wrong means he's not there yet) AND we
# haven't hit the cap.
while ((loc_x[count + 1] != 10 || loc_y[count + 1] != 5) &&
count < max_steps) {
count <- count + 1
# Accumulate onto the PREVIOUS position -- this is the line that
# was originally a silent replacement bug.
loc_x[count + 1] <- loc_x[count] + flip_coin_x(p1, p2)
loc_y[count + 1] <- loc_y[count] + flip_coin_y(p3)
}
# Trim the pre-allocated slots the walk never reached -- otherwise
# they'd sit in the vector as false zeros and show up in a path plot
# as positions he never actually occupied.
loc_x <- loc_x[seq_len(count + 1)]
loc_y <- loc_y[seq_len(count + 1)]
# A count under the cap means he made it home; a count AT the cap
# means the loop ran out of flips without arriving.
if (count < max_steps) {
list(success = TRUE, count = count, loc_x = loc_x, loc_y = loc_y)
} else {
list(success = FALSE, count = count, loc_x = loc_x, loc_y = loc_y)
}
}
One more near-miss: my first version of the success check tested
count > 5000, which can never be true, since the
while loop only ever runs while
count < 5000 in the first place. Checking
count < max_steps instead is the version that actually
distinguishes “arrived early” from “ran out the clock.”
The assignment asks for the average number of flips to get home, conditioning on the person actually returning home — meaning failed walks (the ones that hit the 5,000-flip cap) don’t count toward the average at all.
m <- 5000
# Runs `compute()` m times and returns the average flip count -- but only
# averaged over the walks that actually succeeded. That conditioning is
# the whole point of `counts[successes]` below: it keeps only the flip
# counts whose matching success flag was TRUE, silently dropping the
# ones that hit the cap.
run_sim <- function(p1, p2, p3) {
results <- replicate(m, compute(p1, p2, p3))
successes <- unlist(results["success", ])
counts <- unlist(results["count", ])
mean(counts[successes])
}
Before jumping to 5,000 repetitions and averages, here’s what a single walk actually looks like, and here’s one flip of it worked out by hand so the mechanism is completely concrete before the code takes over: say coin 1 lands heads and coin 2 lands tails — that’s a mixed result, so he stays put east-west; meanwhile coin 3 lands heads, so he moves one step north. One flip, two coin outcomes, one resulting move.
set.seed(1) # required seed for this assignment
m <- 5000
# Baseline case
p_heads <- 0.5
# Scenario A: coin 1 biased toward heads, coins 2 & 3 fair
p_a_1 <- 0.75
p_a_2 <- 0.5
p_a_3 <- 0.5
# Scenario B: coins 1 & 2 both biased toward heads, coin 3 fair
p_b_1 <- 0.75
p_b_2 <- 0.75
p_b_3 <- 0.5
# Run all three scenarios first (same order the results below are quoted
# in), THEN generate the single walk used for the path plot -- keeping
# this order matters for a seeded simulation, since every random draw
# shifts the sequence for everything that comes after it.
avg_flips_all_fair <- run_sim(p_heads, p_heads, p_heads)
avg_flips_a <- run_sim(p_a_1, p_a_2, p_a_3)
avg_flips_b <- run_sim(p_b_1, p_b_2, p_b_3)
baseline_walk <- compute(p_heads, p_heads, p_heads)
par(bg = "#0a0a0a", col.axis = "#f7f1e6", col.lab = "#f7f1e6",
col.main = "#f7f1e6", fg = "#f7f1e6")
plot(baseline_walk$loc_x, baseline_walk$loc_y, type = "l",
col = "#5bcefa", lwd = 1.4,
xlab = "East-West position (steps from the bar)",
ylab = "South-North position (steps from the bar)",
main = "One Drunkard's Walk (all-fair coins)")
grid(col = "#2a2a2a")
points(10, 5, pch = 17, col = "#d4ff00", cex = 2.2)
legend("topleft", legend = "Home (10 east, 5 south)", pch = 17,
col = "#d4ff00", text.col = "#f7f1e6", bty = "n")
That squiggly line is the whole story of zero drift. He wanders, backtracks, loops on himself — there’s no straight shot to the neon triangle marking home. This particular walk took 5000 flips and hit the 5,000-flip cap without arriving. That’s one walk, though — to answer “how long does this typically take,” I need to repeat it thousands of times.
Running run_sim() with all three coins fair, conditioned
only on the walks that actually made it home, the average comes out
to:
1,147.832 flips.
That’s a big number, and it should be — with zero expected drift, getting 10 east and 5 south out of pure noise takes a while, and plenty of individual walks (like some of the ones behind that average) never make it inside 5,000 flips at all.
(Matches Section 2, items 2.1–2.6 on my worksheet.)
Now suppose coin 1 is rigged toward heads — \(Pr(heads) = 0.75\) — while coin 2 and coin 3 stay fair. Before touching the simulation again, I redid the hand math with the new number.
# 2.1 -- P(East) needs BOTH coin 1 (now biased) and coin 2 (still fair)
# to land heads.
p_east_a <- p_a_1 * p_a_2 # .375
# 2.2 -- P(West) needs both to land tails. Coin 1's tail probability
# dropped to .25 now that it's biased toward heads.
p_west_a <- (1 - p_a_1) * (1 - p_a_2) # .125
# 2.3 -- P(Stay) is whatever's left, and it should also equal
# P(H,T) + P(T,H) under the new probabilities -- both ways of computing
# it have to agree.
p_stay_a <- 1 - p_east_a - p_west_a # .50
check_stay_a <- (p_a_1 * (1 - p_a_2)) + ((1 - p_a_1) * p_a_2)
p_stay_a
## [1] 0.5
check_stay_a
## [1] 0.5
# 2.4 -- expected east-west movement per flip, same formula as before,
# just with the updated probabilities plugged in.
e_delta_x_a <- (1 * p_east_a) + (-1 * p_west_a) + (0 * p_stay_a)
e_delta_x_a
## [1] 0.25
Does north-south change? No — because coin 3’s probability is still 50/50. Biasing coin 1 has zero effect on \(P(North)\) or \(P(South)\); those two directions are decided by a completely separate coin.
My prediction, made before running any simulation: \(E[\Delta X] = .25\) is measuring the average eastward steps per flip, meaning it can be reached in less time because it is greater than zero. In other words: there’s now an actual push toward home (unlike the baseline’s zero drift), so I expect the average number of flips to go down.
p_east <- p_heads * p_heads
p_west <- (1 - p_heads) * (1 - p_heads)
east_vals <- c(p_east, p_east_a, NA) # Scenario B filled in below
west_vals <- c(p_west, p_west_a, NA)
par(bg = "#0a0a0a", col.axis = "#f7f1e6", col.lab = "#f7f1e6",
col.main = "#f7f1e6", fg = "#f7f1e6")
mat <- rbind(East = east_vals[1:2], West = west_vals[1:2])
colnames(mat) <- c("All fair", "Scenario A")
barplot(mat, beside = TRUE, col = c("#3ee88f", "#ff9e2c"),
ylim = c(0, 0.6), ylab = "Probability",
main = "P(East) vs. P(West): Baseline vs. One Biased Coin")
legend("topright", legend = rownames(mat), fill = c("#3ee88f", "#ff9e2c"),
text.col = "#f7f1e6", bty = "n")
Now, what the simulation actually found: with coin 1 biased, the conditional average dropped to 42.8432 flips — way down from the baseline’s 1,147.832. My prediction held up: a positive \(E[\Delta X]\) meant an actual push toward home, and the flip count fell hard.
(Matches Section 3, items 3.1–3.4 on my worksheet.)
Now both coin 1 and coin 2 are biased toward heads (\(Pr(heads) = 0.75\) for each), with coin 3 staying fair.
# 3.1
p_east_b <- p_b_1 * p_b_2 # .5625
p_west_b <- (1 - p_b_1) * (1 - p_b_2) # .0625
p_stay_b <- 1 - p_east_b - p_west_b # .375
p_east_b + p_west_b + p_stay_b # sanity check, should be 1
## [1] 1
# 3.2
e_delta_x_b <- (1 * p_east_b) + (-1 * p_west_b) + (0 * p_stay_b)
e_delta_x_b
## [1] 0.5
Is the jump in drift proportional to the single-coin change, or bigger? This took me longest to actually answer – my first pass just restated the direction (both probabilities went up, so drift went up), which isn’t the same as answering whether the size of the increase was proportional. What actually worked was two subtractions: the effect of biasing the first coin, then the effect of biasing the second coin on top of that.
first_coin_effect <- e_delta_x_a - e_delta_x # effect of biasing coin 1 alone
second_coin_effect <- e_delta_x_b - e_delta_x_a # effect of biasing coin 2 on top
first_coin_effect
## [1] 0.25
second_coin_effect
## [1] 0.25
Both effects come out to exactly 0.25. It is proportional — each coin you bias contributes the same fixed amount to \(E[\Delta X]\), because \(P(East)\) and \(P(West)\) combine the two coins’ probabilities multiplicatively, not by just adding them up.
My prediction, again written before simulating: the average number of flips needed to make it home declines as \(E[\Delta X]\) changes based on coin probability, with all-fair needing the most flips and Scenario B needing the least.
p_hh_vals <- c(p_east, p_east_a, p_east_b)
avg_flips_vals <- c(avg_flips_all_fair, avg_flips_a, avg_flips_b)
par(bg = "#0a0a0a", col.axis = "#f7f1e6", col.lab = "#f7f1e6",
col.main = "#f7f1e6", fg = "#f7f1e6")
# Padding on both axes so the point labels below have room to sit above
# and beside the top-left and bottom-right points instead of getting
# clipped by the plot edges.
plot(p_hh_vals, avg_flips_vals, type = "b", pch = 19, cex = 1.5, lwd = 2,
col = "#ff3da6",
xlim = c(min(p_hh_vals) - 0.02, max(p_hh_vals) + 0.05),
ylim = c(0, max(avg_flips_vals) * 1.12),
xlab = "P(East) = P(coin 1 heads and coin 2 heads)",
ylab = "Average flips to reach home",
main = "Average Flips to Home vs. P(East)")
grid(col = "#2a2a2a")
text(p_hh_vals, avg_flips_vals,
labels = c("All fair", "Scenario A", "Scenario B"),
pos = c(4, 3, 2), col = "#f7f1e6", cex = 0.85)
And what the simulation actually found: 21.5012 flips on average — lower than both the baseline and Scenario A, exactly the ordering I predicted. Going from zero bias, to one biased coin, to two biased coins takes the average flip count from 1,147.832, to 42.8432, to 21.5012. A “small” 0.25 bump in one coin’s head probability turns into a massive cut in how long the walk takes, because that bias compounds every single flip, for potentially thousands of flips in a row.
(Matches Section 4, items 4.1–4.3 on my worksheet.)
I don’t just trust a simulation because it ran without errors — I built it to be checkable against the hand math I already trusted, and that’s the whole point of doing the algebra first.
n_check <- 100000
# Test the coin-flip function directly, at high volume, separate from
# full walks -- this checks whether the COINS behave as predicted, which
# doesn't require running any 5,000-step walks at all.
flips_all_fair <- replicate(n_check, flip_coin_x(p_heads, p_heads))
flips_a <- replicate(n_check, flip_coin_x(p_a_1, p_a_2))
flips_b <- replicate(n_check, flip_coin_x(p_b_1, p_b_2))
prop_all_fair <- prop.table(table(flips_all_fair))
prop_a <- prop.table(table(flips_a))
prop_b <- prop.table(table(flips_b))
# The north-south coin is never altered across scenarios, so its
# empirical proportions should stay essentially unchanged too.
flips_coin_3 <- replicate(n_check, flip_coin_y(p_heads))
prop_coin_3 <- prop.table(table(flips_coin_3))
| Scenario | West (−1) | Stay (0) | East (+1) | |
|---|---|---|---|---|
| All fair | predicted | .25 | .50 | .25 |
| All fair | simulated | 0.24951 | 0.50147 | 0.24902 |
| A | predicted | .125 | .50 | .375 |
| A | simulated | 0.12377 | 0.50273 | 0.3735 |
| B | predicted | .0625 | .375 | .5625 |
| B | simulated | 0.06309 | 0.37526 | 0.56165 |
Every simulated proportion lands within about .002 of what I predicted by hand — well inside sampling noise at 100,000 draws. That’s the coin mechanics confirmed independently of anything about walk length.
Predicted direction (E[ΔX]): 0 → .25 → .5 Actual average flips: 1147.8 → 42.8 → 21.5
As predicted, the number of flips needed decreases as \(E[\Delta X]\) increases.
Coin 3’s simulated proportion of north vs. south: 0.5033 south / 0.4967 north — it is unchanged because its probability never changes. It is .5 throughout every draw, across all three scenarios, exactly as it should be since coin 3 is never altered by either bias scenario. If this had drifted noticeably, that would point to a bug somewhere, not a real effect.
The math and the code told the same story from two different directions, and watching them agree is honestly the most convincing part of this whole exercise. A “small” bias — just one coin going from 50/50 to 75/25 — cut the average trip home by over 96%, and biasing a second coin on top of that cut it further, by a proportional amount. None of that is obvious just from staring at “0.75 instead of 0.5.” It only becomes obvious once you actually watch what happens to 5,000 repeated walks.
The bug-hunting mattered just as much as the math. The zero-drift prediction from the very first section — “he has to get lucky, it will take as long to get home as it does to not” — is what let me recognize the accumulation bug as a bug instead of writing it off as bad luck. If I hadn’t done the hand math first, a walk that never left \(\pm 1\) might have just looked like an unlucky drunk guy, instead of broken code.