From the Friday, May 1, 2026 Fiddler on the Proof, verbatim:
A frog is hopping around a chessboard, always from the center of one square to the center of another square. Each square has side length 1, but the board itself is not necessarily 8-by-8. Instead, it’s N-by-N, where N is some large whole number.
Every jump the frog makes must be the same distance, which we’ll call L. The frog wants to make four jumps such that:
What is the smallest jumping distance L for which this is possible?
At first, we check something simple like the knight move that moves 1 square in the positive or negative direction of one dimension, and then 2 squares in the positive or negative direction of the other dimension. That is, from our initial starting point, call it (0,0), we need to choose (1,2) (1, -2), (-1, 2), (-1,-2), (2,1), (2,-1), (-2,1), or (-2,-1) as the next spot.
Let’s explore choosing (1,2) to start. Our next move cannot have a -1 as the first coordinate, nor can it have a -2 as the second coordinate. It also cannot be the same (1,2) because the return path must be the exact opposite resulting in visiting only three squares (a repeat square in the middle) instead of four. That leaves choosing (2,1), (2,-1), (-2,1), or (-2,-1). These choices would place us on (3,3), (3,1), (-1,3), or (-1,1). Since (3,3) and (-1,1) are on the diagonal, or a bishop move from (0,0), those aren’t allowed.
After a little analysis on either (3,1) or (-1,3), you can quickly see that the only options to get back to the origin in two more jumps is to either go in a square loop where each side is \(\sqrt{1^2+2^2}\) or back on a repeat square.
This turns out to be the case for any positive integer \(a\) and \(b\) pair, not just the 1 and 2 option.
Thus, we need a hypotenuse that can have two different sets of integer legs. The first integer length hypotenuse that has this property can be found using the first number on sequence A084646 from the OEIS website, which is 25. That is, \(25^2 = 15^2+20^2\) as well as \(25^2 = 24^2+7^2\). However, the hypotenuse need not be an integer.
Let’s set up some code to find the first hypotenuse length that can be represented two different ways.
i <- 1
squares <- 1
sumSquares <- 2
Found <- FALSE
while(!Found){
i <- i + 1
squares <- c(squares, i^2)
newSumSquares <- i^2+squares
if(any(newSumSquares %in% sumSquares)){
Found <- TRUE
print(newSumSquares[newSumSquares %in% sumSquares])
}
sumSquares <- c(sumSquares, newSumSquares)
}
## [1] 50
We find 50 is the first such number. It can be represented by \(1^2+7^2\) and \(5^2+5^2\). If we analyze this as above, we’ll run into a hiccup. That is, we’ll either begin or end on a \((\pm 5, \pm 5)\) diagonal path from or back to (0,0).
Let’s change the code slightly so that we eliminate the square possibilities (such as the 5x5 above).
i <- 1
squares <- 1
sumSquares <- c()
Found <- FALSE
while(!Found){
i <- i + 1
newSumSquares <- i^2+squares
if(any(newSumSquares %in% sumSquares)){
Found <- TRUE
print(newSumSquares[newSumSquares %in% sumSquares])
}
squares <- c(squares, i^2)
sumSquares <- c(sumSquares, newSumSquares)
}
## [1] 65
Alas, \(\sqrt{65}\) is the hypotenuse we’re after. The square of this hypotenuse can be represented as either \(1^2+8^2\) or \(4^2+7^2\). This shall avoid the bishop diagonal and the rook lines in several different ways. One such way is to have corners of the rhombus at (0,0), (1,8), (5,15), and (4,7) where the frog jumps in that order before going back to (0,0). Not important, but we see that at least a 16x16 chessboard would be needed to get this done.
This is hardly a proof, which may need some graph theory, but the reasoning here should be solid. The length of the frog jump sufficient is \(\sqrt{65} \approx 8.062258\). (And probably necessary).
Here is the extra credit verbatim from the Fiddler on the Proof:
The frog is jumping around the board with the same minimum distance \(L=\sqrt{65}\) that was found above.
But this time, the frog also wants to be able to hop to every location on the chessboard. What is the minimum value of N for which this is possible?
If we let (0,0) be the bottom left of the board and let the frog jump around trying to stay in the smallest \(NxN\) board possible, we quickly find out that if we try and stay within an 8x8 grid (where (7,7) is the furthest corner since 0 is counted as the first tile) using only the \(\pm 4\) and \(pm 7\) combinations, we see that we can only travel to (4,7), (7,4), and (0,0).
Expanding to a 9x9 grid (where (8,8) is the furthest corner), we now get to add the \(pm 1\) and \(\pm 8\) combinations. Here we cannot get to any of the squares defined in the square on the (2,2) to (6,6) diagonal. Take (2,2) for example. You need to add or subtract 7 from either the x or y coordinate at the least and this will put you outside the grid.
Using this logic, we land on a 14x14 grid (where (13,13) is the furthest corner). First, let’s look at the 13x13 grid where (12,12) is the furthest corner defined by the grid. When we examine the middle square (6,6) we again note how we must at least add or subtract 7 from one of the two coordinates, which places it outside the bounds.
With the 14x14 grid, the middle square is defined by the (6,6) to (7.7) diagonal. A move can be found that will take any of these middle squares to another place within the grid. Now it remains to find an actual path that will cover all 14x14 = 196 squares. Here is a function that will find a path that starts at (0,0) and will traverse through all 196 squares and more if necessary.
find_path <- function(v1, v2, N, max_iter = 1e6) {
a <- v1[1]; b <- v1[2]
c <- v2[1]; d <- v2[2]
# Generate all 16 moves
gen_moves <- function(x, y) {
unique(rbind(
c(x,y), c(x,-y), c(-x,y), c(-x,-y),
c(y,x), c(y,-x), c(-y,x), c(-y,-x)
))
}
moves <- rbind(gen_moves(a,b), gen_moves(c,d))
moves <- unique(moves)
# Grid tracking
visited <- matrix(FALSE, nrow = N, ncol = N)
# Helper: check bounds
in_bounds <- function(x,y) {
x >= 0 && x <= N-1 && y >= 0 && y <= N-1
}
# Heuristic: count onward moves
onward_moves <- function(x,y) {
sum(sapply(1:nrow(moves), function(i) {
nx <- x + moves[i,1]
ny <- y + moves[i,2]
in_bounds(nx, ny) && !visited[nx+1, ny+1]
}))
}
path <- list()
iter <- 0
dfs <- function(x, y, step) {
iter <<- iter + 1
if (iter > max_iter) return(FALSE)
visited[x+1, y+1] <<- TRUE
path[[step]] <<- c(x,y)
if (step == N^2) return(TRUE)
# Order moves by heuristic (fewest onward moves first)
next_moves <- lapply(1:nrow(moves), function(i) {
nx <- x + moves[i,1]
ny <- y + moves[i,2]
if (in_bounds(nx, ny) && !visited[nx+1, ny+1]) {
c(nx, ny)
} else NULL
})
next_moves <- Filter(Negate(is.null), next_moves)
if (length(next_moves) == 0) {
visited[x+1, y+1] <<- FALSE
return(FALSE)
}
scores <- sapply(next_moves, function(p) onward_moves(p[1], p[2]))
order_idx <- order(scores)
for (i in order_idx) {
if (dfs(next_moves[[i]][1], next_moves[[i]][2], step + 1)) {
return(TRUE)
}
}
visited[x+1, y+1] <<- FALSE
return(FALSE)
}
success <- dfs(0,0,1)
if (!success) {
return(list(
success = FALSE,
steps = NA,
path = NULL,
message = "No path found within iteration limit"
))
}
return(list(
success = TRUE,
steps = length(path),
path = do.call(rbind, path)
))
}
result14 <- find_path(c(4,7), c(1,8), 14)
result14$path
## [,1] [,2]
## [1,] 0 0
## [2,] 4 7
## [3,] 12 6
## [4,] 8 13
## [5,] 0 12
## [6,] 7 8
## [7,] 8 0
## [8,] 12 7
## [9,] 4 6
## [10,] 0 13
## [11,] 7 9
## [12,] 11 2
## [13,] 3 1
## [14,] 11 0
## [15,] 7 7
## [16,] 3 0
## [17,] 11 1
## [18,] 3 2
## [19,] 10 6
## [20,] 6 13
## [21,] 2 6
## [22,] 10 7
## [23,] 6 0
## [24,] 2 7
## [25,] 10 8
## [26,] 6 1
## [27,] 2 8
## [28,] 1 0
## [29,] 5 7
## [30,] 9 0
## [31,] 13 7
## [32,] 6 11
## [33,] 7 3
## [34,] 11 10
## [35,] 3 11
## [36,] 7 4
## [37,] 11 11
## [38,] 3 10
## [39,] 2 2
## [40,] 10 3
## [41,] 6 10
## [42,] 13 6
## [43,] 6 2
## [44,] 10 9
## [45,] 3 13
## [46,] 7 6
## [47,] 11 13
## [48,] 3 12
## [49,] 7 5
## [50,] 11 12
## [51,] 10 4
## [52,] 2 3
## [53,] 10 2
## [54,] 6 9
## [55,] 13 13
## [56,] 9 6
## [57,] 2 10
## [58,] 6 3
## [59,] 10 10
## [60,] 3 6
## [61,] 7 13
## [62,] 11 6
## [63,] 3 7
## [64,] 7 0
## [65,] 11 7
## [66,] 4 3
## [67,] 0 10
## [68,] 1 2
## [69,] 9 1
## [70,] 2 5
## [71,] 6 12
## [72,] 10 5
## [73,] 2 4
## [74,] 1 12
## [75,] 0 4
## [76,] 4 11
## [77,] 3 3
## [78,] 7 10
## [79,] 11 3
## [80,] 10 11
## [81,] 9 3
## [82,] 8 11
## [83,] 1 7
## [84,] 5 0
## [85,] 12 4
## [86,] 4 5
## [87,] 8 12
## [88,] 12 5
## [89,] 4 4
## [90,] 0 11
## [91,] 1 3
## [92,] 5 10
## [93,] 4 2
## [94,] 12 3
## [95,] 13 11
## [96,] 6 7
## [97,] 10 0
## [98,] 3 4
## [99,] 7 11
## [100,] 0 7
## [101,] 8 8
## [102,] 0 9
## [103,] 8 10
## [104,] 9 2
## [105,] 13 9
## [106,] 12 1
## [107,] 5 5
## [108,] 9 12
## [109,] 13 5
## [110,] 12 13
## [111,] 8 6
## [112,] 4 13
## [113,] 0 6
## [114,] 7 2
## [115,] 11 9
## [116,] 10 1
## [117,] 6 8
## [118,] 2 1
## [119,] 3 9
## [120,] 11 8
## [121,] 7 1
## [122,] 3 8
## [123,] 2 0
## [124,] 9 4
## [125,] 5 11
## [126,] 1 4
## [127,] 2 12
## [128,] 6 5
## [129,] 10 12
## [130,] 2 11
## [131,] 9 7
## [132,] 13 0
## [133,] 6 4
## [134,] 13 8
## [135,] 5 9
## [136,] 13 10
## [137,] 6 6
## [138,] 10 13
## [139,] 11 5
## [140,] 7 12
## [141,] 3 5
## [142,] 2 13
## [143,] 1 5
## [144,] 5 12
## [145,] 9 5
## [146,] 13 12
## [147,] 5 13
## [148,] 1 6
## [149,] 8 2
## [150,] 0 1
## [151,] 4 8
## [152,] 11 4
## [153,] 4 0
## [154,] 5 8
## [155,] 1 1
## [156,] 2 9
## [157,] 9 13
## [158,] 5 6
## [159,] 1 13
## [160,] 8 9
## [161,] 12 2
## [162,] 4 1
## [163,] 0 8
## [164,] 8 7
## [165,] 12 0
## [166,] 5 4
## [167,] 1 11
## [168,] 0 3
## [169,] 8 4
## [170,] 0 5
## [171,] 4 12
## [172,] 8 5
## [173,] 12 12
## [174,] 13 4
## [175,] 9 11
## [176,] 8 3
## [177,] 12 10
## [178,] 13 2
## [179,] 5 3
## [180,] 9 10
## [181,] 13 3
## [182,] 12 11
## [183,] 4 10
## [184,] 12 9
## [185,] 13 1
## [186,] 9 8
## [187,] 1 9
## [188,] 5 2
## [189,] 9 9
## [190,] 1 10
## [191,] 0 2
## [192,] 8 1
## [193,] 12 8
## [194,] 4 9
## [195,] 5 1
## [196,] 1 8
We now have a path that touches every square on the 14x14 grid. Surprisingly (maybe), it does so without repeating a single square. To see this visually, check out the video (pause during last moment to see all squares covered). Please feel free to download and use at your own leisure. The code to produce the video is below the video.
build_trace_grid <- function(path, cols = 14, rows = 14) {
n <- nrow(path)
grid <- expand.grid(
row = 1:rows,
col = 1:cols
)
grid$step <- NA_integer_
grid$x <- NA_integer_
grid$y <- NA_integer_
for (i in seq_len(n)) {
r <- ((i - 1) %% rows) + 1
c <- ((i - 1) %/% rows) + 1
if (c <= cols) {
grid$step[grid$row == r & grid$col == c] <- i
grid$x[grid$row == r & grid$col == c] <- path[i, 1]
grid$y[grid$row == r & grid$col == c] <- path[i, 2]
}
}
grid
}
animate_frog <- function(result, N, outfile = "frog_combo.mp4") {
library(ggplot2)
library(dplyr)
library(gganimate)
library(av)
path <- as.data.frame(result$path)
colnames(path) <- c("x", "y")
path$frame <- seq_len(nrow(path))
board <- expand.grid(x = 0:(N-1), y = 0:(N-1))
board$tile <- (board$x + board$y) %% 2
# build visit accumulation per frame
frames <- lapply(seq_len(nrow(path)), function(i) {
visited <- path[1:i, , drop = FALSE]
visits <- visited %>%
group_by(x, y) %>%
summarise(visits = n(), .groups = "drop")
board_frame <- board %>%
left_join(visits, by = c("x", "y"))
board_frame$visits[is.na(board_frame$visits)] <- 0
board_frame$frame <- i
# NEW: compute alpha as a column (NOT inline)
board_frame$alpha <- dplyr::case_when(
board_frame$visits == 0 ~ 0,
board_frame$visits == 1 ~ 0.4,
board_frame$visits >= 2 ~ 0.9
)
board_frame
})
board_df <- do.call(rbind, frames)
trace_frames <- lapply(seq_len(nrow(path)), function(i) {
trace_df <- build_trace_grid(path, cols = 14, rows = 14)
trace_df$visible <- trace_df$step <= i
trace_df$frame <- i
trace_df
})
trace_df <- do.call(rbind, trace_frames)
# ----------------------------
# SINGLE PLOT, TWO PANELS
# ----------------------------
p <- ggplot() +
# LEFT PANEL: frog board
geom_tile(
data = board_df,
aes(x = x, y = y),
fill = ifelse(board_df$tile == 0, "white", "lightgray"),
color = "grey60"
) +
geom_tile(
data = board_df,
aes(x = x, y = y, alpha = alpha),
fill = "dodgerblue",
color = "grey60"
) +
geom_point(
data = path,
aes(x = x, y = y),
color = "limegreen",
size = 6
) +
# RIGHT PANEL: trace grid
geom_tile(
data = subset(trace_df, visible),
aes(
x = col + (N + 3),
y = 14 - row, # enforces top-to-bottom fill
group = frame
),
fill = "white",
color = "white"
) +
geom_text(
data = subset(trace_df, visible),
aes(
x = col + (N + 3),
y = 14 - row,
label = paste0("(", x, ",", y, ")"),
group = frame
),
size = 3
) +
scale_fill_manual(values = c("0" = "white", "1" = "lightgray"), guide = "none") +
scale_alpha_identity() +
coord_fixed() +
theme_void() +
transition_manual(frame)
animate(
p,
fps = 3,
nframes = nrow(path),
width = 1200,
height = 600,
renderer = av_renderer(outfile)
)
}
animate_frog(result14, N=14, outfile="frog_walk.mp4")