Graphically Plot a Random Walk

Pixels take a hike!

Part 1 - Part 2 - Part 3

Random Walk - Part 1

A random walk is a mathematical process whereby a dot moves step by step round a window, with the direction of each move determined by a probabilistic rule. Below we will write a 2d random walk with an 8 way direction choice for each step.

At the top of the program we define constant values for the window size and the dot size (in pixels) - it is best to choose an odd number for the dot size such as 3 or 5. Next we define a struct to hold the x and y coordinates of the dot as it moves round the screen. Then we define methods for this struct for creating a new instance at start of the run in the centre of the screen, the code to randomly handle which direction the next step will be and finally the code to draw the dot.

In the main program we define the window buffer and then open a window to render the buffer to the screen. After calling Walker::new to create the dot we enter a loop that will perform a walker.step and walker.show to move and draw the dot in the buffer, then display the new buffer to the window. The random walk is fire and forget - that is, once we draw a dot, it is there for ever and it will not change or vanish.

The Rust code

// 8 way random walk with minifb and a 5px dot.
// written by Rich of maths.earth 20250101 
use minifb::{Key, Window, WindowOptions};
use rand::Rng;

// Define the size of the window.
const WIDTH: usize = 800;
const HEIGHT: usize = 600;

// Define the size of the dot as an odd number so the "centre" aligns with the walker's (x, y).
const DOT_SIZE: usize = 3;

// Define struct to store the walker's position.
struct Walker {
    x: usize,
    y: usize,
}

// Define methods for the Walker.
impl Walker {
    fn new() -> Self {
        Walker {
            x: WIDTH / 2,
            y: HEIGHT / 2,
        }
    }

    fn step(&mut self) {
        // 8 directions: 0..8
        let mut rng = rand::thread_rng();
        match rng.gen_range(0..8) {
            0 => { // Right
                if self.x < WIDTH - 1 {
                    self.x += 1;
                }
            }
            1 => { // Left
                if self.x > 0 {
                    self.x -= 1;
                }
            }
            2 => { // Down
                if self.y < HEIGHT - 1 {
                    self.y += 1;
                }
            }
            3 => { // Up
                if self.y > 0 {
                    self.y -= 1;
                }
            }
            4 => { // Diagonal down-right
                if self.x < WIDTH - 1 {
                    self.x += 1;
                }
                if self.y < HEIGHT - 1 {
                    self.y += 1;
                }
            }
            5 => { // Diagonal up-right
                if self.x < WIDTH - 1 {
                    self.x += 1;
                }
                if self.y > 0 {
                    self.y -= 1;
                }
            }
            6 => { // Diagonal down-left
                if self.x > 0 {
                    self.x -= 1;
                }
                if self.y < HEIGHT - 1 {
                    self.y += 1;
                }
            }
            7 => { // Diagonal up-left
                if self.x > 0 {
                    self.x -= 1;
                }
                if self.y > 0 {
                    self.y -= 1;
                }
            }
            _ => {}
        }
    }

    fn show(&self, buffer: &mut [u32]) {
        // Plot a small DOT_SIZE x DOT_SIZE block around (x, y).
        // Make sure we stay within bounds.
        let half = (DOT_SIZE / 2) as isize;

        for dy in -half..=half {
            for dx in -half..=half {
                let px = self.x as isize + dx;
                let py = self.y as isize + dy;

                if px >= 0 && px < WIDTH as isize && py >= 0 && py < HEIGHT as isize {
                    buffer[py as usize * WIDTH + px as usize] = 0x000000; // black
                }
            }
        }
    }
}

fn main() {
    // Create a window with default options.
    let mut window = Window::new(
        "Random Walk (8-way movement)",
        WIDTH,
        HEIGHT,
        WindowOptions::default(),
    )
    .unwrap_or_else(|e| {
        panic!("Failed to create window: {}", e);
    });

    // Create a buffer for the entire screen, initialised to white.
    let mut buffer = vec![0xFFFFFF; WIDTH * HEIGHT];

    // Create our Walker.
    let mut walker = Walker::new();

    // Main event loop.
    while window.is_open() && !window.is_key_down(Key::Escape) {
        // Update the walker's position.
        walker.step();

        // Draw the new position with a larger dot.
        walker.show(&mut buffer);

        // Render the updated buffer.
        window
            .update_with_buffer(&buffer, WIDTH, HEIGHT)
            .unwrap();
    }
}

Cargo.toml

				[package]
name = "randomwalk"
version = "0.1.0"
edition = "2021"

[dependencies]
minifb = "0.27.0"
rand = "0.8"

The image produced:

Mission Accomplished

And there we have it, we can render a random walk to a window and easily modify the code to change the size of the window and dot.