Galton box animation

From Rosetta Code
Revision as of 23:38, 9 September 2024 by Jjuanhdez (talk | contribs)
(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)
Task
Galton box animation
You are encouraged to solve this task according to the task description, using any language you may know.
Example of a Galton Box at the end of animation.


A   Galton device   Sir Francis Galton's device   is also known as a   bean machine,   a   Galton Board,   or a   quincunx.


Description of operation

In a Galton box, there are a set of pins arranged in a triangular pattern.   A number of balls are dropped so that they fall in line with the top pin, deflecting to the left or the right of the pin.   The ball continues to fall to the left or right of lower pins before arriving at one of the collection points between and to the sides of the bottom row of pins.

Eventually the balls are collected into bins at the bottom   (as shown in the image),   the ball column heights in the bins approximate a   bell curve.   Overlaying   Pascal's triangle   onto the pins shows the number of different paths that can be taken to get to each bin.


Task

Generate an animated simulation of a Galton device.


Task requirements
  •   The box should have at least 5 pins on the bottom row.
  •   A solution can use graphics or ASCII animation.
  •   Provide a sample of the output/display such as a screenshot.
  •   There can be one or more balls in flight at the same time.
  •   If multiple balls are in flight, ensure they don't interfere with each other.
  •   A solution should allow users to specify the number of balls, or it should run until full or a preset limit.
  •   Optionally,   display the number of balls.



AutoHotkey

Uses an edit box for the (text based) animation

AutoTrim Off
; User settings
bottompegs := 6
SleepTime  := 200
fallspace  := 30

; create the board
out := (pad2 := Space(bottompegs*2+1)) "`n"
Loop % bottompegs
{
	out .= Space(bottompegs-A_Index+1)
	Loop % A_Index
		out .= "* "
	out .= Space(bottompegs-A_Index+1) . "`n" 
}
StringTrimRight, strboard, out, 1 ; remove last newline
Loop % fallspace-1
	strboard .= "`n" . pad2
strboard .= "`n"
Loop % bottompegs*2+1
	strboard .= "="

; Create Gui
Gui Font, , Consolas
Gui -Caption
Gui Margin, 0, 0
Gui, Add, edit, -VScroll vE, % strboard
Gui Show
Loop
{
	ballX := bottompegs+1, BallY := 1
	strboard := ChangeChar(strboard, BallX, ballY, "O")
	GuiControl,, E, % strboard
	sleep SleepTime
	; Make ball fall and bounce
	Loop % bottompegs
	{
		strboard := ChangeChar(strboard, BallX, BallY, " ")
		ballY += 1
		ballX += RandAdd()
		; MsgBox % ballX ", " ballY
		GuiControl,, E, % strboard := ChangeChar(strboard, ballX, ballY, "O")
		sleep SleepTime
	}
	; now fall to the bottom
	While GetChar(strboard, BallX, BallY+1) = A_Space
	{
		strboard := ChangeChar(strboard, BallX, BallY, " ")
		BallY += 1
		strboard := ChangeChar(strboard, BallX, BallY, "O")
		GuiControl,, E, % strboard
		sleep SleepTime
	}
}
~Esc::
GuiClose:
ExitApp

Space(n){
	If n
		return " " Space(n-1)
	return ""
}
RandAdd(){
	Random, n, 3, 4
	return (n=3 ? -1 : 1)
}

GetChar(s, x, y){
	Loop Parse, s, `n
		if (A_Index = y)
			return SubStr(A_LoopField, x, 1)
}
ChangeChar(s, x, y, c){
	Loop Parse, s, `n
	{
		If (A_Index = y)
		{
			Loop Parse, A_LoopField
				If (A_Index = x)
					out .= c
				else    out .= A_LoopField
		}
		else out .= A_LoopField
		     out .= "`n"
	}
	StringTrimRight, out, out, 1 ; removes the last newline
	return out
}

While the number of pegs, and falling space are configurable, here's output shortly after starting one configuration:

            
      *       
     * *O     
    * * *     
   * * * *    
  * * * * *   
 * * * * * *  
             
             
    O        
  O O        
  O O O O    
O O O O O    
=============

BASIC256

Galton box animation created with BASIC-256
graphsize 150,125
fastgraphics
color black
rect 0,0,graphwidth,graphheight
refresh

N = 10		# number of balls
M = 5		# number of pins in last row
dim ball(N,5)	# (pos_x to center, level, x, y, direction}
dim cnt(M+1)

rad = 6
slow = 0.3
diamond = {0,rad,rad,0,0,-rad,-rad,0}
stepx = {rad/sqr(2),rad/2,rad/2,(1-1/sqr(2))*rad,0}
stepy = {(1-1/sqr(2))*rad,rad/2,rad/2,rad/sqr(2),rad}
CX = graphwidth/2 : CY = graphheight/2
iters = 0

# Draw pins
for i = 1 to M
	y = 3*rad*i
	for j = 1 to i 
		dx = (j-i\2-1)*4*rad + ((i-1)%2)*2*rad
		color purple
		stamp CX+dx,y,1.0,diamond
		color darkpurple
		stamp CX+dx,y,0.6,diamond
	next j
next i
gosub saverefresh

R = 0 : C = 0
font "Tahoma",10,50
do
	# Release ball
	if R<N then
		R = R + 1
		ball[R-1,2] = CX : ball[R-1,3] = rad*(1-stepx[?]) : ball[R-1,4] = 0
		# How many balls are released
		color black
		text 5,5,(R-1)+" balls"
		color green
		text 5,5,(R)+" balls"
	end if
	# Animate balls on this step
	for it = 0 to stepx[?]-1
		for b = 0 to R-1 
			gosub moveball
		next b
		gosub saverefresh
		pause slow/stepx[?]
	next it
	# Where to go on the next step?
	for b = 0 to R-1 
		ball[b,1] = ball[b,1] + 1
		if ball[b,1]<=M then
			if rand>=0.5 then 
				ball[b,4] = 1
			else
				ball[b,4] = -1
			end if
			ball[b,0] = ball[b,0] + ball[b,4]
		else
			if ball[b,4]<>0 then
				gosub eraseball
				i = (ball[b,0]+M)/2
				cnt[i] = cnt[i] + 1
				ball[b,4] = 0
				C = C + 1
			end if
		end if
	next b
	# Draw counter
	color green
	y = 3*rad*(M+1)
	for j = 0 to M
		dx = (j-(M+1)\2)*4*rad + (M%2)*2*rad
		stamp CX+dx,y,{-1.2*rad,0,1.2*rad,0,1.2*rad,2*cnt[j],-1.2*rad,2*cnt[j]}
	next j
	gosub saverefresh
until C >= N
end

moveball:
	if ball[b,1]>M then return
	gosub eraseball
	if ball[b,4]<>0.0 then
		ball[b,2] = ball[b,2]+ball[b,4]*stepx[it]
		ball[b,3] = ball[b,3]+stepy[it]
	else 
		ball[b,3] = ball[b,3]+rad
	end if
	gosub drawball

drawball:
	color darkgreen
	circle ball[b,2],ball[b,3],rad-1
	color green
	circle ball[b,2],ball[b,3],rad-2
	return 

eraseball:
	color black
	circle ball[b,2],ball[b,3],rad-1
	return

saverefresh:
	num$ = string(iters)	
	for k = 1 to 4-length(num$)
		num$ = "0"+num$ 
	next k
	imgsave num$+"-Galton_box_BASIC-256.png", "PNG"
	iters = iters + 1
	refresh
	return

BBC BASIC

      maxBalls% = 10
      DIM ballX%(maxBalls%), ballY%(maxBalls%)
      
      VDU 23,22,180;400;8,16,16,128
      ORIGIN 180,0
      OFF
      
      REM Draw the pins:
      GCOL 9
      FOR row% = 1 TO 7
        FOR col% = 1 TO row%
          CIRCLE FILL 40*col% - 20*row% - 20, 800 - 40*row%, 12
        NEXT
      NEXT row%
      
      REM Animate
      last% = 0
      tick% = 0
      GCOL 3,3
      REPEAT
        IF RND(10) = 5 IF (tick% - last%) > 10 THEN
          FOR ball% = 1 TO maxBalls%
            IF ballY%(ball%) = 0 THEN
              ballX%(ball%) = 0
              ballY%(ball%) = 800
              last% = tick%
              EXIT FOR
            ENDIF
          NEXT
        ENDIF
        FOR ball% = 1 TO maxBalls%
          IF ballY%(ball%) CIRCLE FILL ballX%(ball%), ballY%(ball%), 12
          IF POINT(ballX%(ball%),ballY%(ball%)-10) = 12 OR ballY%(ball%) < 12 THEN
            IF ballY%(ball%) > 500 END
            ballY%(ball%) = 0
          ENDIF
        NEXT
        WAIT 2
        FOR ball% = 1 TO maxBalls%
          IF ballY%(ball%) THEN
            CIRCLE FILL ballX%(ball%), ballY%(ball%), 12
            ballY%(ball%) -= 4
            IF POINT(ballX%(ball%),ballY%(ball%)-10) = 9 THEN
              ballX%(ball%) += 40 * (RND(2) - 1.5)
            ENDIF
          ENDIF
        NEXT
        tick% += 1
      UNTIL FALSE

C

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#define BALLS 1024
int n, w, h = 45, *x, *y, cnt = 0;
char *b;

#define B(y, x) b[(y)*w + x]
#define C(y, x) ' ' == b[(y)*w + x]
#define V(i) B(y[i], x[i])
inline int rnd(int a) { return (rand()/(RAND_MAX/a))%a; }

void show_board()
{
	int i, j;
	for (puts("\033[H"), i = 0; i < h; i++, putchar('\n'))
		for (j = 0; j < w; j++, putchar(' '))
			printf(B(i, j) == '*' ?
				C(i - 1, j) ? "\033[32m%c\033[m" :
				"\033[31m%c\033[m" : "%c", B(i, j));
}

void init()
{
	int i, j;
	puts("\033[H\033[J");
	b = malloc(w * h);
	memset(b, ' ', w * h);

	x = malloc(sizeof(int) * BALLS * 2);
	y = x + BALLS;

	for (i = 0; i < n; i++)
		for (j = -i; j <= i; j += 2)
			B(2 * i+2, j + w/2) = '*';
	srand(time(0));
}

void move(int idx)
{
	int xx = x[idx], yy = y[idx], c, kill = 0, sl = 3, o = 0;

	if (yy < 0) return;
	if (yy == h - 1) { y[idx] = -1; return; }

	switch(c = B(yy + 1, xx)) {
	case ' ':	yy++; break;
	case '*':	sl = 1;
	default:	if (xx < w - 1 && C(yy, xx + 1) && C(yy + 1, xx + 1))
				if (!rnd(sl++)) o = 1;
			if (xx && C(yy, xx - 1) && C(yy + 1, xx - 1))
				if (!rnd(sl++)) o = -1;
			if (!o) kill = 1;
			xx += o;
	}

	c = V(idx); V(idx) = ' ';
	idx[y] = yy, idx[x] = xx;
	B(yy, xx) = c;
	if (kill) idx[y] = -1;
}

int run(void)
{
	static int step = 0;
	int i;
	for (i = 0; i < cnt; i++) move(i);
	if (2 == ++step && cnt < BALLS) {
		step = 0;
		x[cnt] = w/2;
		y[cnt] = 0;
		if (V(cnt) != ' ') return 0;
		V(cnt) = rnd(80) + 43;
		cnt++;
	}
	return 1;
}

int main(int c, char **v)
{
	if (c < 2 || (n = atoi(v[1])) <= 3) n = 5;
	if (n >= 20) n = 20;
	w = n * 2 + 1;
	init();

	do { show_board(), usleep(60000); } while (run());

	return 0;
}

Sample out put at begining of a run:

          *

        *   *

      *   *   *

    *   *   *   *

  *   *   *   *   *

C++

Windows GDI version.

#include "stdafx.h"
#include <windows.h>
#include <stdlib.h>

const int BMP_WID = 410, BMP_HEI = 230, MAX_BALLS = 120;

class myBitmap {
public:
    myBitmap() : pen( NULL ), brush( NULL ), clr( 0 ), wid( 1 ) {}
    ~myBitmap() {
        DeleteObject( pen ); DeleteObject( brush );
        DeleteDC( hdc ); DeleteObject( bmp );
    }
    bool create( int w, int h ) {
        BITMAPINFO bi;
        ZeroMemory( &bi, sizeof( bi ) );
        bi.bmiHeader.biSize        = sizeof( bi.bmiHeader );
        bi.bmiHeader.biBitCount    = sizeof( DWORD ) * 8;
        bi.bmiHeader.biCompression = BI_RGB;
        bi.bmiHeader.biPlanes      = 1;
        bi.bmiHeader.biWidth       =  w;
        bi.bmiHeader.biHeight      = -h;

        HDC dc = GetDC( GetConsoleWindow() );
        bmp = CreateDIBSection( dc, &bi, DIB_RGB_COLORS, &pBits, NULL, 0 );
        if( !bmp ) return false;
        hdc = CreateCompatibleDC( dc );
        SelectObject( hdc, bmp );
        ReleaseDC( GetConsoleWindow(), dc );
        width = w; height = h;
        return true;
    }
    void clear( BYTE clr = 0 ) {
        memset( pBits, clr, width * height * sizeof( DWORD ) );
    }
    void setBrushColor( DWORD bClr ) {
        if( brush ) DeleteObject( brush );
        brush = CreateSolidBrush( bClr );
        SelectObject( hdc, brush );
    }
    void setPenColor( DWORD c ) {
        clr = c; createPen();
    }
    void setPenWidth( int w ) {
        wid = w; createPen();
    }
    HDC getDC() const     { return hdc; }
    int getWidth() const  { return width; }
    int getHeight() const { return height; }
private:
    void createPen() {
        if( pen ) DeleteObject( pen );
        pen = CreatePen( PS_SOLID, wid, clr );
        SelectObject( hdc, pen );
    }
    HBITMAP bmp;
    HDC     hdc;
    HPEN    pen;
    HBRUSH  brush;
    void    *pBits;
    int     width, height, wid;
    DWORD   clr;
};
class point {
public:
    int x; float y;
    void set( int a, float b ) { x = a; y = b; }
};
typedef struct {
    point position, offset;
    bool alive, start;
}ball;
class galton {
public :
    galton() {
        bmp.create( BMP_WID, BMP_HEI );
        initialize();
    }
    void setHWND( HWND hwnd ) { _hwnd = hwnd; }
    void simulate() {
        draw(); update(); Sleep( 1 );
    }
private:
    void draw() {
        bmp.clear();
        bmp.setPenColor( RGB( 0, 255, 0 ) );
        bmp.setBrushColor( RGB( 0, 255, 0 ) );
        int xx, yy;
        for( int y = 3; y < 14; y++ ) {
            yy = 10 * y;
            for( int x = 0; x < 41; x++ ) {
                xx = 10 * x;
                if( pins[y][x] )
                    Rectangle( bmp.getDC(), xx - 3, yy - 3, xx + 3, yy + 3 );
            }
        }
        bmp.setPenColor( RGB( 255, 0, 0 ) );
        bmp.setBrushColor( RGB( 255, 0, 0 ) );
        ball* b; 
        for( int x = 0; x < MAX_BALLS; x++ ) {
            b = &balls[x];
            if( b->alive )
                Rectangle( bmp.getDC(), static_cast<int>( b->position.x - 3 ), static_cast<int>( b->position.y - 3 ), 
                                        static_cast<int>( b->position.x + 3 ), static_cast<int>( b->position.y + 3 ) );
        }
        for( int x = 0; x < 70; x++ ) {
            if( cols[x] > 0 ) {
                xx = 10 * x;
                Rectangle( bmp.getDC(), xx - 3, 160, xx + 3, 160 + cols[x] );
            }
        }
        HDC dc = GetDC( _hwnd );
        BitBlt( dc, 0, 0, BMP_WID, BMP_HEI, bmp.getDC(), 0, 0, SRCCOPY );
        ReleaseDC( _hwnd, dc );
    }
    void update() {
        ball* b;
        for( int x = 0; x < MAX_BALLS; x++ ) {
            b = &balls[x];
            if( b->alive ) {
                b->position.x += b->offset.x; b->position.y += b->offset.y;
                if( x < MAX_BALLS - 1 && !b->start && b->position.y > 50.0f ) {
                    b->start = true;
                    balls[x + 1].alive = true;
                }
                int c = ( int )b->position.x, d = ( int )b->position.y + 6;
                if( d > 10 || d < 41 ) {
                    if( pins[d / 10][c / 10] ) {
                        if( rand() % 30 < 15 ) b->position.x -= 10;
                        else b->position.x += 10;
                    }
                }
                if( b->position.y > 160 ) {
                    b->alive = false;
                    cols[c / 10] += 1;
                }
            }
        }
    }
    void initialize() {
        for( int x = 0; x < MAX_BALLS; x++ ) {
            balls[x].position.set( 200, -10 );
            balls[x].offset.set( 0, 0.5f );
            balls[x].alive = balls[x].start = false;
        }
        balls[0].alive = true;
        for( int x = 0; x < 70; x++ )
            cols[x] = 0;
        for( int y = 0; y < 70; y++ )
            for( int x = 0; x < 41; x++ )
                pins[x][y] = false;
        int p;
        for( int y = 0; y < 11; y++ ) {
            p = ( 41 / 2 ) - y;
            for( int z = 0; z < y + 1; z++ ) {
                pins[3 + y][p] = true;
                p += 2;
            }
        }
    }
    myBitmap bmp;
    HWND _hwnd;
    bool pins[70][40];
    ball balls[MAX_BALLS];
    int cols[70];
};
class wnd {
public:
    int wnd::Run( HINSTANCE hInst ) {
        _hInst = hInst;
        _hwnd = InitAll();
        _gtn.setHWND( _hwnd );
        ShowWindow( _hwnd, SW_SHOW );
        UpdateWindow( _hwnd );
        MSG msg;
        ZeroMemory( &msg, sizeof( msg ) );
        while( msg.message != WM_QUIT ) {
            if( PeekMessage( &msg, NULL, 0, 0, PM_REMOVE ) != 0 ) {
                TranslateMessage( &msg );
                DispatchMessage( &msg );
            } else _gtn.simulate();
        }
        return UnregisterClass( "_GALTON_", _hInst );
    }
private:
    static int WINAPI wnd::WndProc( HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam ) {
        switch( msg ) {
            case WM_DESTROY: PostQuitMessage( 0 ); break;
            default:
                return static_cast<int>( DefWindowProc( hWnd, msg, wParam, lParam ) );
        }
        return 0;
    }
    HWND InitAll() {
        WNDCLASSEX wcex;
        ZeroMemory( &wcex, sizeof( wcex ) );
        wcex.cbSize           = sizeof( WNDCLASSEX );
        wcex.style           = CS_HREDRAW | CS_VREDRAW;
        wcex.lpfnWndProc   = ( WNDPROC )WndProc;
        wcex.hInstance     = _hInst;
        wcex.hCursor       = LoadCursor( NULL, IDC_ARROW );
        wcex.hbrBackground = ( HBRUSH )( COLOR_WINDOW + 1 );
        wcex.lpszClassName = "_GALTON_";
        RegisterClassEx( &wcex );
        RECT rc;
        SetRect( &rc, 0, 0, BMP_WID, BMP_HEI );
        AdjustWindowRect( &rc, WS_CAPTION, FALSE );
        return CreateWindow( "_GALTON_", ".: Galton Box -- PJorente :.", WS_SYSMENU, CW_USEDEFAULT, 0, rc.right - rc.left, rc.bottom - rc.top, NULL, NULL, _hInst, NULL );
    }
    HINSTANCE _hInst;
    HWND      _hwnd;
    galton    _gtn;
};
int APIENTRY WinMain( HINSTANCE hInstance, HINSTANCE hPrevInstance, LPTSTR lpCmdLine, int nCmdShow ) {
    srand( GetTickCount() );
    wnd myWnd; 
    return myWnd.Run( hInstance );
}

Clojure

(def n 8)
(def balls* (atom [{:x n :y 0}]))
(def board* (atom (vec (repeat (inc n) (vec (repeat (inc (* 2 n)) " "))))))
(doseq [y (range (inc n))
        i (range y)]
  (swap! board* assoc-in [y (+ (- n y) (* 2 i) 1)] "^"))
(def histogram* (atom (vec (repeat (inc (* 2 n)) 0))))

(loop [frame 0]
  (print "\033[0;0f\033[2J")
  (doseq [row @board*] (println (apply str row)))
  (let [depth (inc (apply max (map #(quot % 8) @histogram*)))]
    (dotimes [y depth]
      (doseq [i @histogram*]
        (print (nth " ▁▂▃▄▅▆▇█" (min 8 (max 0 (- i (* (- depth y 1) 8)))))))
      (print "\n")))
  (println "\n")
  (flush)
  (doseq [[i {:keys [x y]}] (map-indexed vector @balls*)]
    (swap! board* assoc-in [y x] " ")
    (let [[new-x new-y] [(if (< 0.5 (rand)) (inc x) (dec x)) (inc y)]]
      (if (> new-y n)
        (do (swap! histogram* update x inc)
            (swap! balls* assoc i {:x n :y 0}))
        (do (swap! board* assoc-in [new-y new-x] "*")
            (swap! balls* assoc i {:x new-x :y new-y})))))
  (Thread/sleep 200)
  (when (< (count @balls*) n) (swap! balls* conj {:x n :y 0}))
  (when (< frame 200) (recur (inc frame))))

Sample output:

                 
        ^*       
       ^*^       
      ^ ^ ^      
     ^ ^ ^*^     
    ^*^ ^ ^ ^    
   ^ ^ ^*^ ^ ^   
  ^ ^ ^ ^*^ ^ ^  
 ^ ^ ^*^ ^ ^ ^ ^ 
      ▅ ▅        
      █ █ ▂      
      █ █ █      
    ▃ █ █ █      
    █ █ █ █ █    
▁ ▇ █ █ █ █ █ ▃ ▁

D

To keep the code simpler some corner cases are ignored.

import std.stdio, std.algorithm, std.random, std.array;

enum int boxW = 41, boxH = 37; // Galton box width and height.
enum int pinsBaseW = 19;       // Pins triangle base size.
enum int nMaxBalls = 55;       // Number of balls.

static assert(boxW >= 2 && boxH >= 2);
static assert((boxW - 4) >= (pinsBaseW * 2 - 1));
static assert((boxH - 3) >= pinsBaseW);
enum centerH = pinsBaseW + (boxW - (pinsBaseW * 2 - 1)) / 2 - 1;

enum Cell : char { empty  = ' ',
                   ball   = 'o',
                   wall   = '|',
                   corner = '+',
                   floor  = '-',
                   pin    = '.' }

Cell[boxW][boxH] box; // Galton box. Will be printed upside-down.

struct Ball {
    int x, y; // Position.

    this(in int x_, in int y_) nothrow @safe @nogc
    in {
        assert(box[y_][x_] == Cell.empty);
    } body {
        this.x = x_;
        this.y = y_;
        box[y][x] = Cell.ball;
    }

    nothrow const @safe @nogc invariant {
        assert(x >= 0 && x < boxW && y >= 0 && y < boxH);
        assert(box[y][x] == Cell.ball);
    }

    void doStep() {
        if (y <= 0)
            return; // Reached the bottom of the box.

        final switch (box[y - 1][x]) with (Cell) {
            case empty:
                box[y][x] = Cell.empty;
                y--;
                box[y][x] = Cell.ball;
                break;
            case ball, wall, corner, floor:
                // It's frozen. (It always piles on other balls).
                break;
            case pin:
                box[y][x] = Cell.empty;
                y--;
                if (box[y][x - 1] == Cell.empty && box[y][x + 1] == Cell.empty) {
                    x += uniform(0, 2) * 2 - 1;
                    box[y][x] = Cell.ball;
                    return;
                } else if (box[y][x - 1] == Cell.empty) {
                    x++;
                } else {
                    x--;
                }
                box[y][x] = Cell.ball;
                break;
        }
    }
}

void initializeBox() {
    // Set ceiling and floor:
    box[0][] = Cell.corner ~ [Cell.floor].replicate(boxW - 2) ~ Cell.corner;
    box[$ - 1][] = box[0][];

    // Set walls:
    foreach (immutable r; 1 .. boxH - 1)
        box[r][0] = box[r][$ - 1] = Cell.wall;

    // Set pins:
    foreach (immutable nPins; 1 .. pinsBaseW + 1)
        foreach (pin; 0 .. nPins)
            box[boxH - 2 - nPins][centerH + 1 - nPins + pin * 2] = Cell.pin;
}

void drawBox() {
    foreach_reverse (const ref row; box)
        writefln("%(%c%)", row);
}

void main() {
    initializeBox;
    Ball[] balls;

    foreach (const i; 0 .. nMaxBalls + boxH) {
        writefln("\nStep %d:", i);
        if (i < nMaxBalls)
            balls ~= Ball(centerH, boxH - 2); // Add ball.
        drawBox;

        // Next step for the simulation.
        // Frozen balls are kept in balls array for simplicity.
        foreach (ref b; balls)
            b.doStep;
    }
}
Output:
Step 0:
+---------------------------------------+
|                   o                   |
|                   .                   |
|                  . .                  |
|                 . . .                 |
|                . . . .                |
|               . . . . .               |
|              . . . . . .              |
|             . . . . . . .             |
|            . . . . . . . .            |
|           . . . . . . . . .           |
|          . . . . . . . . . .          |
|         . . . . . . . . . . .         |
|        . . . . . . . . . . . .        |
|       . . . . . . . . . . . . .       |
|      . . . . . . . . . . . . . .      |
|     . . . . . . . . . . . . . . .     |
|    . . . . . . . . . . . . . . . .    |
|   . . . . . . . . . . . . . . . . .   |
|  . . . . . . . . . . . . . . . . . .  |
| . . . . . . . . . . . . . . . . . . . |
|                                       |
|                                       |
|                                       |
|                                       |
|                                       |
|                                       |
|                                       |
|                                       |
|                                       |
|                                       |
|                                       |
|                                       |
|                                       |
|                                       |
|                                       |
+---------------------------------------+

...

Step 39:
+---------------------------------------+
|                   o                   |
|                  o.                   |
|                  . .o                 |
|                 . .o.                 |
|                .o. . .                |
|               .o. . . .               |
|              . . .o. . .              |
|             . . .o. . . .             |
|            . . .o. . . . .            |
|           . . . .o. . . . .           |
|          . . . . . .o. . . .          |
|         . . . . . .o. . . . .         |
|        . . . .o. . . . . . . .        |
|       . . . . . .o. . . . . . .       |
|      . . . . . . .o. . . . . . .      |
|     . . . . . .o. . . . . . . . .     |
|    . . . .o. . . . . . . . . . . .    |
|   . . . . . . . . .o. . . . . . . .   |
|  . . . . . .o. . . . . . . . . . . .  |
| . . . . . . . .o. . . . . . . . . . . |
|                      o                |
|                  o                    |
|                  o                    |
|                    o                  |
|                o                      |
|                  o                    |
|                  o                    |
|            o                          |
|                    o                  |
|                    o                  |
|                    o                  |
|              o         o              |
|              o                        |
|              o o o                    |
|              o o     o                |
+---------------------------------------+

...

Step 91:
+---------------------------------------+
|                                       |
|                   .                   |
|                  . .                  |
|                 . . .                 |
|                . . . .                |
|               . . . . .               |
|              . . . . . .              |
|             . . . . . . .             |
|            . . . . . . . .            |
|           . . . . . . . . .           |
|          . . . . . . . . . .          |
|         . . . . . . . . . . .         |
|        . . . . . . . . . . . .        |
|       . . . . . . . . . . . . .       |
|      . . . . . . . . . . . . . .      |
|     . . . . . . . . . . . . . . .     |
|    . . . . . . . . . . . . . . . .    |
|   . . . . . . . . . . . . . . . . .   |
|  . . . . . . . . . . . . . . . . . .  |
| . . . . . . . . . . . . . . . . . . . |
|                                       |
|                                       |
|                o                      |
|                o   o                  |
|                o   o                  |
|                o   o                  |
|                o   o                  |
|                o   o                  |
|                o   o   o              |
|                o o o   o              |
|                o o o   o              |
|              o o o o   o              |
|              o o o o o o o            |
|          o o o o o o o o o o          |
|      o   o o o o o o o o o o          |
+---------------------------------------+

EasyLang

Run it

sysconf topleft
# 
proc drawpins . .
   for i to 9
      for j to i
         move (15 - i) * 3 + j * 6 i * 6 + 2
         circle 0.7
      .
   .
.
color 555
drawpins
background -1
# 
len box[] 10
len x[] 10
len y[] 10
# 
proc showbox . .
   for i to 10
      x = i * 6 + 15
      for j to box[i]
         move x 100 - j * 4 + 2
         circle 2
      .
   .
.
proc init . .
   for i to 10
      box[i] = 0
      x[i] = 0
   .
.
# 
color 543
on timer
   if busy = 0 and random 4 = 1
      busy = 1
      for i to 10
         if x[i] = 0
            x[i] = 48
            y[i] = 2
            break 1
         .
      .
   else
      busy = 0
   .
   clear
   showbox
   for i to 10
      x = x[i]
      if x > 0
         if y[i] <= 56
            y[i] += 2
            if y[i] mod 6 = 2
               x += 3 * (random 2 * 2 - 3)
               x[i] = x
            .
         else
            idx = (x - 15) / 6
            y[i] += 4
            if y[i] >= 96 - box[idx] * 4
               x[i] = 0
               box[idx] += 1
               if box[idx] > 10
                  init
                  break 1
               .
            .
         .
         move x y[i]
         circle 2
      .
   .
   timer 0.1
.
timer 0

Elm

import Html.App exposing (program)
import Time exposing (Time, every, millisecond)
import Color exposing (Color, black, red, blue, green)
import Collage exposing (collage)
import Collage exposing (collage,polygon, filled, move, Form, circle)
import Element exposing (toHtml)
import Html exposing (Attribute, Html, text, div, input, button)
import Html.Attributes as A exposing (type', min, placeholder, value, style, disabled)
import Html.Events exposing (onInput, targetValue, onClick)
import Dict exposing (Dict, get, insert)
import String exposing (toInt)
import Result exposing (withDefault)
import Random.Pcg as Random exposing (Seed, bool, initialSeed, independentSeed, step, map)

width = 500
height = 600
hscale = 10.0
vscale = hscale * 2
margin = 30
levelCount = 12
radius = hscale/ 2.0

type State = InBox Int Int Seed | Falling Int Float Float Float | Landed Int Float

type Coin = Coin State Color

colorCycle : Int -> Color
colorCycle i =
  case i % 3 of
    0 -> red
    1 -> blue
    _ -> green

initCoin : Int -> Seed -> Coin
initCoin indx seed = Coin (InBox 0 0 seed) (colorCycle indx)

drawCoin : Coin -> Form
drawCoin (Coin state color) = 
  let dropLevel = toFloat (height//2 - margin)
      (level, shift, distance) = 
        case state of
          InBox level shift seed -> (level, shift, 0)
          Falling shift distance _ _-> (levelCount, shift, distance)
          Landed shift distance -> (levelCount, shift, distance)
      position = 
        (             hscale * toFloat shift
        , dropLevel - vscale * (toFloat level) - distance + radius / 2.0)

  in radius |> circle |> filled color |> move position 

drawGaltonBox : List Form
drawGaltonBox = 
  let levels = [0..levelCount-1]
 
      -- doubles :
      -- [0,2,4,6,8...]
      doubles = List.map (\n -> 2 * n) levels

      -- sequences :
      -- [[0], [0,2], [0,2,4], [0,2,4,6], [0,2,4,6,8],...]
      sequences = case List.tail (List.scanl (::) [] (doubles)) of
        Nothing -> []
        Just ls -> ls

      -- galtonCoords :
      -- [                            (0,0), 
      --                       (-1,1),      (1,1), 
      --                (-2,2),       (0,2),      (2,2), 
      --         (-3,3),       (-1,3),      (1,3),      (3,3), 
      --  (-4,4),       (-2,4),       (0,4),      (2,4),      (4,4), ...]
      galtonCoords = 
        List.map2 
          (\ls level -> List.map (\n -> (n - level, level)) ls) 
          sequences 
          levels
        |> List.concat

      peg = polygon [(0,0), (-4, -8), (4, -8)] |> filled black 

      apex = toFloat (height//2 - margin)

  in List.map (\(x,y) -> move (hscale*toFloat x,  apex - vscale*toFloat y) peg) galtonCoords

coinsInBin : Int -> Dict Int Int -> Int
coinsInBin binNumber bins = 
  case get binNumber bins of
    Nothing -> 0
    Just n -> n

addToBins : Int -> Dict Int Int -> Dict Int Int
addToBins binNumber bins = 
  insert binNumber (coinsInBin binNumber bins + 1) bins

updateCoin : (Coin, Dict Int Int) -> (Coin, Dict Int Int)
updateCoin (Coin state color as coin, bins) = 
  case state of
    InBox level shift seed ->
      let deltaShift = map (\b -> if b then 1 else -1) bool
          (delta, newSeed) = step deltaShift seed
          newShift = shift+delta
          newLevel = (level)+1
      in if (newLevel < levelCount) then
           (Coin (InBox newLevel newShift newSeed) color, bins)
         else -- transition to falling
           let maxDrop = toFloat (height - 2 * margin) - toFloat (levelCount) * vscale
               floor = maxDrop - toFloat (coinsInBin newShift bins) * (radius*2 + 1)
           in (Coin (Falling newShift -((vscale)/2.0) 10 floor) color, addToBins newShift bins)

    Falling shift distance velocity floor -> 
      let newDistance = distance + velocity
      in if (newDistance < floor) then
           (Coin (Falling shift newDistance (velocity + 1) floor) color, bins)
         else -- transtion to landed
           (Coin (Landed shift floor) color, bins)

    Landed _ _ -> (coin, bins) -- unchanged

type alias Model = 
  { coins : List Coin
  , bins : Dict Int Int
  , count : Int
  , started : Bool
  , seedInitialized : Bool
  , seed : Seed
  }

init : (Model, Cmd Msg)
init =
  ( { coins = []
    , bins = Dict.empty
    , count = 0
    , started = False
    , seedInitialized = False
    , seed = initialSeed 45 -- This will not get used.  Actual seed used is time dependent and set when the first coin drops.
    }, Cmd.none)

type Msg = Drop Time | Tick Time | SetCount String | Go

update : Msg -> Model -> (Model, Cmd Msg)
update action model = 
  case action of
    Go ->
      ({model | started = model.count > 0}, Cmd.none)

    SetCount countString -> 
      ({ model | count = toInt countString |> withDefault 0 }, Cmd.none)

    Drop t -> 
      if (model.started && model.count > 0) then
          let newcount = model.count - 1
              seed' =  if model.seedInitialized then model.seed else initialSeed (truncate t)
              (seed'', coinSeed) = step independentSeed seed'
          in ({ model  
              | coins = initCoin (truncate t) coinSeed :: model.coins
              , count = newcount
              , started = newcount > 0
              , seedInitialized = True
              , seed = seed''}, Cmd.none)
      else
         (model, Cmd.none)

    Tick _ -> 
      -- foldr to execute update, append to coins, replace bins
      let (updatedCoins, updatedBins) =
        List.foldr (\coin (coinList, bins) -> 
                       let (updatedCoin, updatedBins) = updateCoin (coin, bins) 
                       in (updatedCoin :: coinList, updatedBins))
                   ([], model.bins)
                   model.coins
      in ({ model | coins = updatedCoins, bins = updatedBins}, Cmd.none)

view : Model -> Html Msg
view model = 
  div []
    [ input
        [ placeholder "How many?"
        , let showString = if model.count > 0 then model.count |> toString else ""
          in value showString
        , onInput SetCount
        , disabled model.started
        , style [ ("height", "20px") ]
        , type' "number"
        , A.min "1"
        ]
        []

     , button
        [ onClick Go 
        , disabled model.started
        , style [ ("height", "20px") ]
        ]
        [ Html.text "GO!" ]

     , let coinForms = (List.map (drawCoin) model.coins)
       in collage width height (coinForms ++ drawGaltonBox) |> toHtml
    ]

subscriptions model =
    Sub.batch
        [ every (40*millisecond) Tick
        , every (200*millisecond) Drop
        ]

main =
  program 
      { init = init
      , view = view
      , update = update
      , subscriptions = subscriptions
      }

Link to live demo: http://dc25.github.io/galtonBoxAnimationElm/ . Follow the link, enter a number and press the GO button.

Factor

Works with: Factor version 0.99 development release 2019-03-17
USING: accessors arrays calendar colors combinators
combinators.short-circuit fonts fry generalizations kernel
literals locals math math.ranges math.vectors namespaces opengl
random sequences timers ui ui.commands ui.gadgets
ui.gadgets.worlds ui.gestures ui.pens.solid ui.render ui.text ;
IN: rosetta-code.galton-box-animation

CONSTANT: pegs $[ 20 300 40 <range> ]
CONSTANT: speed 90
CONSTANT: balls 140
CONSTANT: peg-color  T{ rgba f 0.60 0.4 0.60 1.0 }
CONSTANT: ball-color T{ rgba f 0.80 1.0 0.20 1.0 }
CONSTANT: slot-color T{ rgba f 0.00 0.2 0.40 1.0 }
CONSTANT: bg-color   T{ rgba f 0.02 0.0 0.02 1.0 }

CONSTANT: font $[
    monospace-font
    t >>bold?
    T{ rgba f 0.80 1.0 0.20 1.0 } >>foreground
    T{ rgba f 0.02 0.0 0.02 1.0 } >>background
]

TUPLE: galton < gadget balls { frame initial: 1 } ;

DEFER: on-tick

: <galton-gadget> ( -- gadget )
    galton new bg-color <solid> >>interior V{ } clone >>balls
    dup [ on-tick ] curry f speed milliseconds <timer>
    start-timer ;

: add-ball ( gadget -- )
    dup frame>> balls <
    [ { 250 -20 } swap balls>> [ push ] keep ] when drop ;

: draw-msg ( -- )
    { 10 10 }
    [ font "Press <space> for new animation" draw-text ]
    with-translation ;

: draw-slots ( -- )
    slot-color gl-color { 70 350 } { 70 871 }
    10 [ 2dup gl-line [ { 40 0 } v+ ] bi@ ] times 2drop
    { 70 871 } { 430 871 } gl-line ;

: diamond-side ( loc1 loc2 loc3 -- )
    [ v+ dup ] [ v+ gl-line ] bi* ;

: draw-diamond ( loc color -- )
    gl-color {
        [ { 0 -10 } { 10 10 } ]
        [ { 10 0 } { -10 10 } ]
        [ { 0 10 } { -10 -10 } ]
        [ { -10 0 } { 10 -10 } ]
    } [ diamond-side ] map-compose cleave ;

: draw-peg-row ( loc n -- )
    <iota> [ 40 * 0 2array v+ peg-color draw-diamond ] with
    each ;

: draw-peg-triangle ( -- )
    { 250 40 } 1
    8 [ 2dup draw-peg-row [ { -20 40 } v+ ] dip 1 + ] times
    2drop ;

: draw-balls ( gadget -- )
    balls>> [ ball-color draw-diamond ] each ;

: rand-side ( loc -- loc' ) { { 20 20 } { -20 20 } } random v+ ;

:: collide? ( GADGET BALL -- ? )
    BALL second :> y
    BALL { 0 20 } v+ :> tentative
    { [ y 860 = ] [ tentative GADGET balls>> member? ] } 0|| ;

:: update-ball ( GADGET BALL -- BALL' )
    {
        { [ BALL second pegs member? ] [ BALL rand-side ] }
        { [ GADGET BALL collide? ] [ BALL ] }
        [ BALL { 0 20 } v+ ]
    } cond ;

: update-balls ( gadget -- )
    dup '[ [ _ swap update-ball ] map ] change-balls drop ;

: on-tick ( gadget -- )
    {
        [ dup frame>> odd? [ add-ball ] [ drop ] if ]
        [ relayout-1 ] [ update-balls ]
        [ [ 1 + ] change-frame drop ]
    } cleave ;

M: galton pref-dim* drop { 500 900 } ;

M: galton draw-gadget*
    draw-peg-triangle draw-msg draw-slots draw-balls ;

: com-new ( gadget -- ) V{ } clone >>balls 1 >>frame drop ;

galton "gestures" f {
    { T{ key-down { sym " " } } com-new }
} define-command-map

MAIN-WINDOW: galton-box-animation
    {
        { title "Galton Box Animation" }
        { window-controls 
            { normal-title-bar close-button minimize-button } }
    } <galton-gadget> >>gadgets ;
Output:

Image taken from the program mid-animation: [1]

FreeBASIC

Console version

Translation of: Go
Const boxW As Integer = 41      ' Galton box width
Const boxH As Integer = 37      ' Galton box height
Const pinsBaseW As Integer = 19 ' Pins triangle base
Const nMaxBalls As Integer = 55 ' Number of balls

Const centerH As Integer = pinsBaseW + (boxW - pinsBaseW * 2 + 1) / 2 - 1

Const empty As String = " "
Const ball As String = "o"
Const wall As String = "|"
Const corner As String = "+"
Const floor As String = "-"
Const pin As String = "."

Type Ball
    x As Integer
    y As Integer
End Type

Dim Shared box(boxH - 1, boxW - 1) As String

Sub initializeBox()
    Dim As Integer r, c, nPins
    ' Set ceiling and floor
    box(0, 0) = corner
    box(0, boxW - 1) = corner
    For c = 1 To boxW - 2
        box(0, c) = floor
    Next
    For c = 0 To boxW - 1
        box(boxH - 1, c) = box(0, c)
    Next
    
    ' Set walls
    For r = 1 To boxH - 2
        box(r, 0) = wall
        box(r, boxW - 1) = wall
    Next
    
    ' Set rest to empty initially
    For r = 1 To boxH - 2
        For c = 1 To boxW - 2
            box(r, c) = empty
        Next
    Next
    
    ' Set pins
    For nPins = 1 To pinsBaseW
        For c = 0 To nPins - 1
            box(boxH - 2 - nPins, centerH + 1 - nPins + c * 2) = pin
        Next
    Next
End Sub

Sub drawBox()
    Dim As Integer r, c
    For r = boxH - 1 To 0 Step -1
        For c = 0 To boxW - 1
            Print box(r, c);
        Next
        Print
    Next
End Sub

Function newBall(x As Integer, y As Integer) As Ball
    If box(y, x) <> empty Then
        Print "Tried to create a new ball in a non-empty cell. Program terminated."
        End
    End If
    Dim b As Ball
    b.x = x
    b.y = y
    box(y, x) = ball
    Return b
End Function

Sub doStep(b As Ball)
    If b.y <= 0 Then
        Exit Sub ' Reached the bottom of the box
    End If
    Dim As String cell = box(b.y - 1, b.x)
    Select Case cell
    Case empty
        box(b.y, b.x) = empty
        b.y -= 1
        box(b.y, b.x) = ball
    Case pin
        box(b.y, b.x) = empty
        b.y -= 1
        If box(b.y, b.x - 1) = empty And box(b.y, b.x + 1) = empty Then
            b.x += Int(Rnd * 2) * 2 - 1
            box(b.y, b.x) = ball
            Exit Sub
        Elseif box(b.y, b.x - 1) = empty Then
            b.x += 1
        Else
            b.x -= 1
        End If
        box(b.y, b.x) = ball
    Case Else
        ' It's frozen - it always piles on other balls
    End Select
End Sub

Dim As Ball balls()
Dim As Integer i, j, ballCount

initializeBox()
For i = 0 To nMaxBalls + boxH - 1
    Print !"\nStep"; i; ":"
    If i < nMaxBalls Then
        ballCount += 1
        Redim Preserve balls(ballCount - 1)
        balls(ballCount - 1) = newBall(centerH, boxH - 2) ' add ball
    End If
    drawBox()
    
    ' Next step for the simulation
    ' Frozen balls are kept in balls array for simplicity
    For j = 0 To ballCount - 1
        doStep(balls(j))
    Next
Next

Sleep
Output:
Similar to Go entry.

GUI version

Translation of: PureBasic
Sample display of FreeBASIC solution
Const pegRadius As Integer = 4
Const pegSize As Integer = pegRadius * 2 + 1
Const pegSize2 As Integer = pegSize * 2
Const delay As Integer = 1
Dim Shared As Integer alto, ancho, histogramSize, ball

Sub eventLoop()
    Dim As String event
    Do
        event = Inkey
        If event <> "" Then End
    Loop Until event = ""
End Sub

Sub animateActual(x1 As Single, y1 As Single, x2 As Single, y2 As Single, steps As Integer)
    Dim As Single x, y, xstep, ystep, lastX, lastY
    x = x1
    y = y1
    xstep = (x2 - x1) / steps
    ystep = (y2 - y1) / steps
    For i As Integer = 1 To steps
        lastX = x
        lastY = y
        Circle (x, y), pegRadius, Rgb(255, 0, 0), , , , F
        eventLoop()
        Sleep delay
        Circle (x, y), pegRadius, Rgb(235, 235, 235), , , , F
        eventLoop()
        x += xstep
        y += ystep
    Next
End Sub

Function drawBall(xpos As Integer, ypos As Integer) As Boolean
    Static As Integer ballcounts()
    If xpos > Ubound(ballcounts) Then
        Redim Preserve ballcounts(xpos) As Integer
    End If
    ballcounts(xpos) += 1
    animateActual(xpos, ypos, xpos, alto - ballcounts(xpos) * pegSize, 20)
    Circle (xpos, alto - ballcounts(xpos) * pegSize), pegRadius, Rgb(255, 0, 0), , , , F
    eventLoop()
    Return Iif(ballcounts(xpos) <= histogramSize, True, False)
End Function

Sub animate(x1 As Single, y1 As Single, x2 As Single, y2 As Single)
    animateActual(x1, y1, x2, y1, 4)
    animateActual(x2, y1, x2, y2, 10)
End Sub

Function galton(pegRows As Integer) As Integer
    Dim As Integer i, xpos, ypos, oldX, oldY
    oldX = ancho / 2 - pegSize / 2
    xpos = oldX
    oldY = pegSize * 3
    ypos = oldY
    animateActual(oldX, 0, xpos, ypos, 10)
    For i = 1 To pegRows
        If Int(Rnd * 2) Then
            xpos += pegSize
        Else
            xpos -= pegSize
        End If
        ypos += pegSize2
        animate(oldX, oldY, xpos, ypos)
        oldX = xpos
        oldY = ypos
    Next
    Return drawBall(xpos, ypos)
End Function

Sub setupWindow(numRows As Integer, ballCount As Integer)
    Dim As Integer i, j, xpos, ypos
    ancho = (2 * numRows + 2) * pegSize + 50
    histogramSize = (ballCount + 2) / 3
    If histogramSize > 500 / pegSize Then histogramSize = 500 / pegSize
    alto = ancho + histogramSize * pegSize + 50
    Screenres ancho, alto, 32
    Windowtitle "Galton box"
    Line (0, 0) - Step(ancho, alto), Rgb(235, 235, 235), BF
    For i = 1 To numRows
        ypos = i * pegSize2 + pegSize * 2
        xpos = ancho / 2 - (i - 1) * pegSize - pegSize / 2
        For j = 1 To i
            Circle (xpos, ypos), pegRadius, Rgb(0, 0, 139), , , , F
            xpos += pegSize2
        Next
    Next
    For i As Integer = 1 To numRows
        Line ((numRows - i + 1) * pegSize2 - pegSize / 2 + 24, alto - pegSize - histogramSize * pegSize) - Step(1, histogramSize * pegSize), Rgb(0, 0, 0)
    Next
End Sub

Dim pegRows As Integer = 10
Dim ballCount As Integer
Do
    Input "How many balls to drop? ", ballCount
Loop Until ballCount > 0

Randomize Timer
setupWindow(pegRows, ballCount)
eventLoop()
For ball = 1 To ballCount
    If Not galton(pegRows) Then Exit For
Next

Color Rgb(255, 0, 0), Rgb(235, 235, 235)
Print Spc(5); "Ended after "; Str(ball-1); " balls"

Do: eventLoop(): Loop

Go

Translation of: D
package main

import (
    "fmt"
    "math/rand"
    "time"
)

const boxW = 41      // Galton box width
const boxH = 37      // Galton box height.
const pinsBaseW = 19 // Pins triangle base.
const nMaxBalls = 55 // Number of balls.

const centerH = pinsBaseW + (boxW-pinsBaseW*2+1)/2 - 1

const (
    empty  = ' '
    ball   = 'o'
    wall   = '|'
    corner = '+'
    floor  = '-'
    pin    = '.'
)

type Ball struct{ x, y int }

func newBall(x, y int) *Ball {
    if box[y][x] != empty {
        panic("Tried to create a new ball in a non-empty cell. Program terminated.")
    }
    b := Ball{x, y}
    box[y][x] = ball
    return &b
}

func (b *Ball) doStep() {
    if b.y <= 0 {
        return // Reached the bottom of the box.
    }
    cell := box[b.y-1][b.x]
    switch cell {
    case empty:
        box[b.y][b.x] = empty
        b.y--
        box[b.y][b.x] = ball
    case pin:
        box[b.y][b.x] = empty
        b.y--
        if box[b.y][b.x-1] == empty && box[b.y][b.x+1] == empty {
            b.x += rand.Intn(2)*2 - 1
            box[b.y][b.x] = ball
            return
        } else if box[b.y][b.x-1] == empty {
            b.x++
        } else {
            b.x--
        }
        box[b.y][b.x] = ball
    default:
        // It's frozen - it always piles on other balls.
    }
}

type Cell = byte

/* Galton box. Will be printed upside down. */
var box [boxH][boxW]Cell

func initializeBox() {
    // Set ceiling and floor
    box[0][0] = corner
    box[0][boxW-1] = corner
    for i := 1; i < boxW-1; i++ {
        box[0][i] = floor
    }
    for i := 0; i < boxW; i++ {
        box[boxH-1][i] = box[0][i]
    }

    // Set walls
    for r := 1; r < boxH-1; r++ {
        box[r][0] = wall
        box[r][boxW-1] = wall
    }

    // Set rest to empty initially
    for i := 1; i < boxH-1; i++ {
        for j := 1; j < boxW-1; j++ {
            box[i][j] = empty
        }
    }

    // Set pins
    for nPins := 1; nPins <= pinsBaseW; nPins++ {
        for p := 0; p < nPins; p++ {
            box[boxH-2-nPins][centerH+1-nPins+p*2] = pin
        }
    }
}

func drawBox() {
    for r := boxH - 1; r >= 0; r-- {
        for c := 0; c < boxW; c++ {
            fmt.Printf("%c", box[r][c])
        }
        fmt.Println()
    }
}

func main() {
    rand.Seed(time.Now().UnixNano())
    initializeBox()
    var balls []*Ball
    for i := 0; i < nMaxBalls+boxH; i++ {
        fmt.Println("\nStep", i, ":")
        if i < nMaxBalls {
            balls = append(balls, newBall(centerH, boxH-2)) // add ball
        }
        drawBox()

        // Next step for the simulation.
        // Frozen balls are kept in balls slice for simplicity
        for _, b := range balls {
            b.doStep()
        }
    }
}
Output:

Sample output (showing last step only):

Step 91 :
+---------------------------------------+
|                                       |
|                   .                   |
|                  . .                  |
|                 . . .                 |
|                . . . .                |
|               . . . . .               |
|              . . . . . .              |
|             . . . . . . .             |
|            . . . . . . . .            |
|           . . . . . . . . .           |
|          . . . . . . . . . .          |
|         . . . . . . . . . . .         |
|        . . . . . . . . . . . .        |
|       . . . . . . . . . . . . .       |
|      . . . . . . . . . . . . . .      |
|     . . . . . . . . . . . . . . .     |
|    . . . . . . . . . . . . . . . .    |
|   . . . . . . . . . . . . . . . . .   |
|  . . . . . . . . . . . . . . . . . .  |
| . . . . . . . . . . . . . . . . . . . |
|                                       |
|                                       |
|                o                      |
|                o                      |
|                o o                    |
|                o o                    |
|                o o                    |
|              o o o                    |
|              o o o                    |
|              o o o o   o              |
|              o o o o   o              |
|            o o o o o o o              |
|            o o o o o o o              |
|          o o o o o o o o              |
|          o o o o o o o o o            |
+---------------------------------------+

Haskell

import Data.Map hiding (map, filter)
import Graphics.Gloss
import Control.Monad.Random

data Ball = Ball { position :: (Int, Int), turns :: [Int] }

type World = ( Int           -- number of rows of pins
             , [Ball]        -- sequence of balls
             , Map Int Int ) -- counting bins

updateWorld :: World -> World
updateWorld (nRows, balls, bins)
  | y < -nRows-5  = (nRows, map update bs, bins <+> x)
  | otherwise     = (nRows, map update balls, bins)
  where
    (Ball (x,y) _) : bs = balls

    b <+> x = unionWith (+) b (singleton x 1)

    update (Ball (x,y) turns)
      | -nRows <= y && y < 0 = Ball (x + head turns, y - 1) (tail turns)
      | otherwise            = Ball (x, y - 1) turns
        
drawWorld :: World -> Picture
drawWorld (nRows, balls, bins) = pictures [ color red ballsP
                                          , color black binsP
                                          , color blue pinsP ]
  where ballsP = foldMap (disk 1) $ takeWhile ((3 >).snd) $ map position balls        
        binsP  = foldMapWithKey drawBin bins
        pinsP  = foldMap (disk 0.2) $ [1..nRows] >>= \i ->
                                          [1..i] >>= \j -> [(2*j-i-1, -i-1)]

        disk r pos = trans pos $ circleSolid (r*10)
        drawBin x h = trans (x,-nRows-7)
                    $ rectangleUpperSolid 20 (-fromIntegral h)
        trans (x,y) = Translate (20 * fromIntegral x) (20 * fromIntegral y)

startSimulation :: Int -> [Ball] -> IO ()
startSimulation nRows balls = simulate display white 50 world drawWorld update
  where display = InWindow "Galton box" (400, 400) (0, 0)
        world = (nRows, balls, empty)
        update _ _ = updateWorld

main = evalRandIO balls >>= startSimulation 10
  where balls = mapM makeBall [1..]
        makeBall y = Ball (0, y) <$> randomTurns
        randomTurns = filter (/=0) <$> getRandomRs (-1, 1)

Icon and Unicon

The code here is adapted from the Unicon Book.

link graphics

global pegsize, pegsize2, height, width, delay

procedure main(args)    # galton box simulation from Unicon book
   pegsize2 := (pegsize := 10) * 2    # pegs & steps
   delay := 2                         # ms delay
   setup_galtonwindow(pegsize)
   n := integer(args[1]) | 100        # balls to drop 
   every 1 to n do galton(pegsize)
   WDone()
end

procedure setup_galtonwindow(n)  # Draw n levels of pegs, 
local xpos, ypos, i, j
   # Pegboard size is 2n-1 square
   # Expected max value of histogram is (n, n/2)/2^n 
   # ... approximate with something simpler?

   height := n*n/2*pegsize + (width := (2*n+1)*pegsize)
   Window("size=" || width || "," || height, "fg=grayish-white")
   WAttrib("fg=dark-grey")
   every ypos := (i := 1 to n) * pegsize2 do {
      xpos := width/2 - (i - 1) * pegsize - pegsize/2 - pegsize2
      every 1 to i do
         FillArc(xpos +:= pegsize2, ypos, pegsize, pegsize)
      }
   WAttrib("fg=black","drawop=reverse")      # set drawing mode for balls
end

procedure galton(n)                          # drop a ball into the galton box
local xpos, ypos, oldx, oldy
   xpos := oldx := width/2 - pegsize/2
   ypos := oldy := pegsize
   every 1 to n do {                         # For every ball...
      xpos +:= ((?2 = 1) | -1) * pegsize     # +/- pegsize
      animate(.oldx, .oldy, oldx := xpos, oldy := ypos +:= pegsize2)
      }
   animate(xpos, ypos, xpos, ypos + 40)      # Now the ball falls ...
   animate(xpos, ypos+40, xpos, ypos + 200)  # ... to the floor
   draw_ball(xpos)                           # Record this ball
end

procedure animate(xfrom, yfrom, xto, yto)
   animate_actual(xfrom, yfrom, xto, yfrom, 4)
   animate_actual(xto, yfrom, xto, yto, 10)
end


procedure animate_actual(xfrom, yfrom, xto, yto, steps) # attribs already set
local x, y, xstep, ystep, lastx, lasty
   x -:= xstep := (xto - (x := xfrom))/steps
   y -:= ystep := (yto - (y := yfrom))/steps
   every 1 to steps do {      
      FillArc(lastx := x +:= xstep, lasty := y +:= ystep, pegsize, pegsize)
      WDelay(delay)      # wait in ms
      FillArc(x, y, pegsize, pegsize)
      }
end

procedure draw_ball(x)                      
static ballcounts
initial ballcounts := table(0)
   FillArc(x, height-(ballcounts[x] +:= 1)*pegsize, pegsize, pegsize)
end

J

First, we need to represent our pins:

initpins=: '* ' {~ '1'&i.@(-@|. |."_1 [: ":@-.&0"1 <:~/~)@i.

For example:

   initpins 4
   *   
  * *  
 * * * 
* * * *

Note that we could introduce other pin arrangements, for example a Sierpinski triangle:

initSpins=: [: }.@|. (1- 2&^@>:) ]\ [: ,] (,~ ,.~)@]^:[ ,: bind '* '

... but this will not be too interesting to use, because of the lack of interior pins for the balls to bounce off of.

Anyways, once we have that, we can add balls to our picture:

init=: ' ',. ' ',.~ ] ,~ ' ',~ ' o' {~  (# ' ' ~: 1&{.)

For example:

   3 (init initpins) 4
    o    
    o    
    o    
         
    *    
   * *   
  * * *  
 * * * *

Now we just need some way of updating our datastructure.

We will need a mechanism to shift a ball left or right if it's above a pin:

bounce=: (C.~ ] <"1@:+ 0 1 -~/~ ? @: (2"0))"1 [: I. 'o*'&E."1&.|:

And, a mechanism to make the balls fall:

shift=: 4 :0
  fill=. {.0#,y
  x |.!.fill y
)

And then we need to separate out the balls from the pins, so the balls fall and the pins do not. Note also that in this representation, balls will have to fall when they bounce because they cannot occupy the same space that a pin occupies.

We will also want some way of preventing the balls from falling forever. For this task it's probably sufficient to introduce a baseline just deep enough to hold the stacks (which have passed through the pins) and have later balls instantly fall as close as they can to the baseline once they are passed the pins.

pins=: '*'&=
balls=: 'o'&=

bounce=: (C.~ 0 1 <@(-/~) [: (+ ?@2:"0) I.)"1 

nxt=: ' ',~ [: clean ' *o' {~ pins + 2 * _1 shift balls bounce balls *. 1 shift pins

clean2=: ({. , -.&' '"1&.|:&.|.@}.)~ 1 + >./@(# | '*' i:~"1 |:)
clean1=: #~ 1 1 -.@E. *./"1@:=&' '
clean=: clean1@clean2

For example:

   nxt nxt 3 (init initpins) 4
         
    o    
    o    
   o*    
   * *   
  * * *  
 * * * *

Or, showing an entire animation sequence:

   nxt&.>^:a: <7 (init ' ',.' ',.~ initpins) 5
┌─────────────┬─────────────┬─────────────┬─────────────┬─────────────┬─────────────┬─────────────┬─────────────┬─────────────┬─────────────┬─────────────┬─────────────┬─────────────┬─────────────┐
│      o      │             │             │             │             │             │             │             │             │             │             │             │             │             │
│      o      │      o      │      o      │      o      │      o      │      o      │      o      │      o      │      *o     │      *      │      *      │      *      │      *      │      *      │
│      o      │      o      │      o      │      o      │      o      │      o      │      o      │      *o     │     *o*     │     *o*     │     * *     │     * *     │     * *     │     * *     │
│      o      │      o      │      o      │      o      │      o      │      o      │      *o     │     * *o    │    * * *o   │    * *o*    │    * *o*    │    * * *    │    * * *    │    * * *    │
│      o      │      o      │      o      │      o      │      o      │      *o     │     * *o    │    * *o*    │   * *o* *   │   * * * *o  │   * *o* *   │   * *o* *   │   * * * *   │   * * * *   │
│      o      │      o      │      o      │      o      │     o*      │    o* *     │    *o* *    │   * *o* *   │  * * *o* *  │  * *o* * *  │  * * * * *o │  * * *o* *  │  * *o* * *  │  * * * * *  │
│      o      │      o      │      o      │      *o     │     *o*     │    * *o*    │   * * *o*   │  * * *o* *  │       o     │       o     │       o     │       o     │       o     │       o     │
│             │      o      │      *o     │     * *o    │    * *o*    │   * *o* *   │  * * *o* *  │       o     │       o     │       o     │       o     │       o     │       o     │       o     │
│      *      │      *      │     * *     │    * * *    │   * * * *   │  * * * * *  │             │             │             │       o     │     o o     │     o o   o │       o     │     o o     │
│     * *     │     * *     │    * * *    │   * * * *   │  * * * * *  │             │             │             │             │             │             │             │     o o   o │     o o   o │
│    * * *    │    * * *    │   * * * *   │  * * * * *  │             │             │             │             │             │             │             │             │             │             │
│   * * * *   │   * * * *   │  * * * * *  │             │             │             │             │             │             │             │             │             │             │             │
│  * * * * *  │  * * * * *  │             │             │             │             │             │             │             │             │             │             │             │             │
│             │             │             │             │             │             │             │             │             │             │             │             │             │             │
└─────────────┴─────────────┴─────────────┴─────────────┴─────────────┴─────────────┴─────────────┴─────────────┴─────────────┴─────────────┴─────────────┴─────────────┴─────────────┴─────────────┘

Java

The balls keep track of where they are, and we just have to move them down and print. You might easily adjust this to take command line input for the numbers of pins and balls. I'm sure that this could be a lot shorter...

import java.util.Random;
import java.util.List;
import java.util.ArrayList;

public class GaltonBox {
    public static void main( final String[] args ) {
        new GaltonBox( 8, 200 ).run();
    }

    private final int        m_pinRows;
    private final int        m_startRow;
    private final Position[] m_balls;
    private final Random     m_random = new Random();

    public GaltonBox( final int pinRows, final int ballCount ) {
        m_pinRows  = pinRows;
        m_startRow = pinRows + 1;
        m_balls    = new Position[ ballCount ];

        for ( int ball = 0; ball < ballCount; ball++ )
            m_balls[ ball ] = new Position( m_startRow, 0, 'o' );
    }

    private static class Position {
        int  m_row;
        int  m_col;
        char m_char;

        Position( final int row, final int col, final char ch ) {
            m_row  = row;
            m_col  = col;
            m_char = ch;
        }
    }

    public void run() {
        for ( int ballsInPlay = m_balls.length; ballsInPlay > 0;  ) {
            ballsInPlay = dropBalls();
            print();
        }
    }

    private int dropBalls() {
        int ballsInPlay = 0;
        int ballToStart = -1;

        // Pick a ball to start dropping
        for ( int ball = 0; ball < m_balls.length; ball++ )
            if ( m_balls[ ball ].m_row == m_startRow )
                ballToStart = ball;

        // Drop balls that are already in play
        for ( int ball = 0; ball < m_balls.length; ball++ )
            if ( ball == ballToStart ) {
                m_balls[ ball ].m_row = m_pinRows;
                ballsInPlay++;
            }
            else if ( m_balls[ ball ].m_row > 0 && m_balls[ ball ].m_row != m_startRow ) {
                m_balls[ ball ].m_row -= 1;
                m_balls[ ball ].m_col += m_random.nextInt( 2 );
                if ( 0 != m_balls[ ball ].m_row )
                    ballsInPlay++;
            }

        return ballsInPlay;
    }

    private void print() {
        for ( int row = m_startRow; row --> 1;  ) {
            for ( int ball = 0; ball < m_balls.length; ball++ )
                if ( m_balls[ ball ].m_row == row )
                    printBall( m_balls[ ball ] );
            System.out.println();
            printPins( row );
        }
        printCollectors();
        System.out.println();
    }

    private static void printBall( final Position pos ) {
        for ( int col = pos.m_row + 1; col --> 0;  )
            System.out.print( ' ' );
        for ( int col = 0; col < pos.m_col; col++ )
            System.out.print( "  " );
        System.out.print( pos.m_char );
    }

    private void printPins( final int row ) {
        for ( int col = row + 1; col --> 0;  )
            System.out.print( ' ' );
        for ( int col = m_startRow - row; col --> 0;  )
            System.out.print( ". " );
        System.out.println();
    }

    private void printCollectors() {
        final List<List<Position>> collectors = new ArrayList<List<Position>>();

        for ( int col = 0; col < m_startRow; col++ ) {
            final List<Position> collector = new ArrayList<Position>();

            collectors.add( collector );
            for ( int ball = 0; ball < m_balls.length; ball++ )
                if ( m_balls[ ball ].m_row == 0 && m_balls[ ball ].m_col == col )
                    collector.add( m_balls[ ball ] );
        }

        for ( int row = 0, rows = longest( collectors ); row < rows; row++ ) {
            for ( int col = 0; col < m_startRow; col++ ) {
                final List<Position> collector = collectors.get( col );
                final int            pos       = row + collector.size() - rows;

                System.out.print( '|' );
                if ( pos >= 0 )
                    System.out.print( collector.get( pos ).m_char );
                else
                    System.out.print( ' ' );
            }
            System.out.println( '|' );
        }
    }

    private static final int longest( final List<List<Position>> collectors ) {
        int result = 0;

        for ( final List<Position> collector : collectors )
            result = Math.max( collector.size(), result );

        return result;
    }
}
Output:

When only five balls have begun to fall through the pins:

         o
         . 
          o
        . . 
           o
       . . . 
        o
      . . . . 
         o
     . . . . . 

    . . . . . . 

   . . . . . . . 

  . . . . . . . . 

Later, some balls have arrived in the collectors:

         o
         . 
          o
        . . 
         o
       . . . 
          o
      . . . . 
         o
     . . . . . 
          o
    . . . . . . 
           o
   . . . . . . . 
        o
  . . . . . . . . 
| | | | | |o| | | |
| | | |o|o|o| | | |

Note that the collectors are as deep as required.

Finally, all the balls are in the collectors:

         . 

        . . 

       . . . 

      . . . . 

     . . . . . 

    . . . . . . 

   . . . . . . . 

  . . . . . . . . 
| | | | |o| | | | |
| | | | |o| | | | |
| | | | |o| | | | |
| | | | |o| | | | |
| | | | |o| | | | |
| | | | |o| | | | |
| | | | |o| | | | |
| | | | |o| | | | |
| | | |o|o| | | | |
| | | |o|o| | | | |
| | | |o|o| | | | |
| | | |o|o| | | | |
| | | |o|o| | | | |
| | | |o|o| | | | |
| | | |o|o|o| | | |
| | | |o|o|o| | | |
| | | |o|o|o| | | |
| | | |o|o|o| | | |
| | | |o|o|o| | | |
| | | |o|o|o| | | |
| | | |o|o|o| | | |
| | | |o|o|o| | | |
| | | |o|o|o| | | |
| | | |o|o|o| | | |
| | | |o|o|o| | | |
| | | |o|o|o| | | |
| | | |o|o|o| | | |
| | | |o|o|o| | | |
| | | |o|o|o| | | |
| | | |o|o|o| | | |
| | | |o|o|o|o| | |
| | | |o|o|o|o| | |
| | | |o|o|o|o| | |
| | | |o|o|o|o| | |
| | | |o|o|o|o| | |
| | | |o|o|o|o| | |
| | | |o|o|o|o| | |
| | |o|o|o|o|o| | |
| | |o|o|o|o|o| | |
| | |o|o|o|o|o| | |
| | |o|o|o|o|o| | |
| | |o|o|o|o|o| | |
| | |o|o|o|o|o| | |
| | |o|o|o|o|o| | |
| | |o|o|o|o|o| | |
| | |o|o|o|o|o| | |
| | |o|o|o|o|o| | |
| | |o|o|o|o|o| | |
| | |o|o|o|o|o| | |
| | |o|o|o|o|o| | |
| | |o|o|o|o|o|o| |
| | |o|o|o|o|o|o| |
| | |o|o|o|o|o|o| |
| |o|o|o|o|o|o|o| |
| |o|o|o|o|o|o|o| |
| |o|o|o|o|o|o|o| |

JavaScript

Works with NodeJs

const readline = require('readline');

/**
 * Galton Box animation
 * @param {number} layers The number of layers in the board
 * @param {number} balls The number of balls to pass through
 */
const galtonBox = (layers, balls) => {
  const speed = 100;
  const ball = 'o';
  const peg = '.';
  const result = [];

  const sleep = ms => new Promise(resolve => {
    setTimeout(resolve,ms)
  });

  /**
   * The board is represented as a 2D array.
   * @type {Array<Array<string>>}
   */
  const board = [...Array(layers)]
      .map((e, i) => {
        const sides = Array(layers - i).fill(' ');
        const a = Array(i + 1).fill(peg).join(' ').split('');
        return [...sides, ...a, ...sides];
      });

  /**
   * @return {Array<string>}
   */
  const emptyRow = () => Array(board[0].length).fill(' ');

  /**
   * @param {number} i
   * @returns {number}
   */
  const bounce = i => Math.round(Math.random()) ? i - 1 : i + 1;

  /**
   * Prints the current state of the board and the collector
   */
  const show = () => {
    readline.cursorTo(process.stdout, 0, 0);
    readline.clearScreenDown(process.stdout);
    board.forEach(e => console.log(e.join('')));
    result.reverse();
    result.forEach(e => console.log(e.join('')));
    result.reverse();
  };


  /**
   * Collect the result.
   * @param {number} idx
   */
  const appendToResult = idx => {
    const row = result.find(e => e[idx] === ' ');
    if (row) {
      row[idx] = ball;
    } else {
      const newRow = emptyRow();
      newRow[idx] = ball;
      result.push(newRow);
    }
  };

  /**
   * Move the balls through the board
   * @returns {boolean} True if the there are balls in the board.
   */
  const iter = () => {
    let hasNext = false;
    [...Array(bordSize)].forEach((e, i) => {
      const rowIdx = (bordSize - 1) - i;
      const idx = board[rowIdx].indexOf(ball);
      if (idx > -1) {
        board[rowIdx][idx] = ' ';
        const nextRowIdx = rowIdx + 1;
        if (nextRowIdx < bordSize) {
          hasNext = true;
          const nextRow = board[nextRowIdx];
          nextRow[bounce(idx)] = ball;
        } else {
          appendToResult(idx);
        }
      }
    });
    return hasNext;
  };

  /**
   * Add a ball to the board.
   * @returns {number} The number of balls left to add.
   */
  const addBall = () => {
    board[0][apex] = ball;
    balls = balls - 1;
    return balls;
  };

  board.unshift(emptyRow());
  result.unshift(emptyRow());

  const bordSize = board.length;
  const apex = board[1].indexOf(peg);

  /**
   * Run the animation
   */
  (async () => {
    while (addBall()) {
      await sleep(speed).then(show);
      iter();
    }
    await sleep(speed).then(show);
    while (iter()) {
      await sleep(speed).then(show);
    }
    await sleep(speed).then(show);
  })();


};

galtonBox(12, 50);
Output:
                         
            .            
           . .           
          . . .          
         . . . .         
        . . . . .        
       . . . . . .       
      . . . . . . .      
     . . . . . . . .     
    . . . . . . . . .    
   . . . . . . . . . .   
  . . . . . . . . . . .  
 . . . . . . . . . . . . 
          o              
          o              
          o              
          o              
          o   o          
          o   o          
          o   o          
          o o o          
          o o o          
          o o o o        
          o o o o        
        o o o o o        
        o o o o o o      
      o o o o o o o      
      o o o o o o o o   

Julia

This is a proof of concept code. It does not use external packages. The ASCII animation is running in the stdout terminal, which has to be at least 28 character wide, and 40 character high, and accept ANSI control sequences for positioning the cursor, and clearing the screen (e.g. the Windows console, or ConEmu).

6 pins in 6 rows are hard coded. The next ball is released at the press of the Enter key. Keep it depressed for continuous running. The balls are randomly deflected left or right at hitting the pins, and they fall to the bins at the bottom, which extend downwards. The timer function sets the speed of the animation. Change the "interval" parameter to larger values for slower movement. Pressing x then Enter exits, other keys are ignored.

Works with: Julia version 1.0
using Random
function drawball(timer)
    global r, c, d
    print("\e[$r;$(c)H ")       # clear last ball position (r,c)
    if (r+=1) > 14
        close(timer)
        b = (bin[(c+2)>>2] += 1)# update count in bin
        print("\e[$b;$(c)Ho")   # lengthen bar of balls in bin
    else
        r in 3:2:13 && c in 17-r:4:11+r && (d = 2bitrand()-1)
        print("\e[$r;$(c+=d)Ho")# show ball moving in direction d
    end
end

print("\e[2J")                  # clear screen
for r = 3:2:13, c = 17-r:4:11+r # 6 pins in 6 rows
    print("\e[$r;$(c)H^")       # draw pins
end
print("\e[15;2H-------------------------")

bin = fill(15,7)                # positions of top of bins
while "x" != readline() >= ""   # x-Enter: exit, {keys..}Enter: next ball
    global r,c,d = 0,14,0
    t = Timer(drawball, 0, interval=0.1)
    while r < 15 sleep(0.01) end
    print("\e[40;1H")           # move cursor far down
end
Output:
             ^

           ^   ^

         ^   ^   ^

       ^   ^   ^   ^

     ^   ^   ^   ^   ^

   ^   ^   ^   ^   ^   ^

--------------------------
 o   o   o   o   o   o   o
     o   o   o   o   o
         o   o   o   o
         o   o   o   o
         o   o   o   o
         o   o   o
             o   o
             o   o
             o   o
             o   o
             o
             o
             o
             o
             o

Kotlin

Translation of: D
// version 1.2.10

import java.util.Random

val boxW = 41       // Galton box width.
val boxH = 37       // Galton box height.
val pinsBaseW = 19  // Pins triangle base.
val nMaxBalls = 55  // Number of balls.

val centerH = pinsBaseW + (boxW - pinsBaseW * 2 + 1) / 2 - 1
val rand = Random()

enum class Cell(val c: Char) {
    EMPTY(' '),
    BALL('o'),
    WALL('|'),
    CORNER('+'),
    FLOOR('-'),
    PIN('.')
}

/* Galton box. Will be printed upside down. */
val box = List(boxH) { Array<Cell>(boxW) { Cell.EMPTY } }

class Ball(var x: Int, var y: Int) {

    init {
        require(box[y][x] == Cell.EMPTY)
        box[y][x] = Cell.BALL
    }

    fun doStep() {
        if (y <= 0) return  // Reached the bottom of the box.
        val cell = box[y - 1][x]
        when (cell) {
            Cell.EMPTY -> {
                box[y][x] = Cell.EMPTY
                y--
                box[y][x] = Cell.BALL
            }

            Cell.PIN -> {
                box[y][x] = Cell.EMPTY
                y--
                if (box[y][x - 1] == Cell.EMPTY && box[y][x + 1] == Cell.EMPTY) {
                    x += rand.nextInt(2) * 2 - 1
                    box[y][x] = Cell.BALL
                    return
                }
                else if (box[y][x - 1] == Cell.EMPTY) x++
                else x--
                box[y][x] = Cell.BALL
            }

            else -> {
                // It's frozen - it always piles on other balls.
            }
        }
    }
}

fun initializeBox() {
    // Set ceiling and floor:
    box[0][0] = Cell.CORNER
    box[0][boxW - 1] = Cell.CORNER
    for (i in 1 until boxW - 1) box[0][i] = Cell.FLOOR
    for (i in 0 until boxW) box[boxH - 1][i] = box[0][i]

    // Set walls:
    for (r in 1 until boxH - 1) {
        box[r][0] = Cell.WALL
        box[r][boxW - 1] = Cell.WALL
    }

    // Set pins:
    for (nPins in 1..pinsBaseW) {
        for (pin in 0 until nPins) {
            box[boxH - 2 - nPins][centerH + 1 - nPins + pin * 2] = Cell.PIN
        }
    }
}

fun drawBox() {
    for (row in box.reversed()) {
        for (i in row.indices) print(row[i].c)
        println()
    }
}

fun main(args: Array<String>) {
    initializeBox()
    val balls = mutableListOf<Ball>()
    for (i in 0 until nMaxBalls + boxH) {
        println("\nStep $i:")
        if (i < nMaxBalls) balls.add(Ball(centerH, boxH - 2))  // Add ball.
        drawBox()

        // Next step for the simulation.
        // Frozen balls are kept in balls list for simplicity
        for (b in balls) b.doStep()
    }
}

Sample output (showing final step only):

Step 91:
+---------------------------------------+
|                                       |
|                   .                   |
|                  . .                  |
|                 . . .                 |
|                . . . .                |
|               . . . . .               |
|              . . . . . .              |
|             . . . . . . .             |
|            . . . . . . . .            |
|           . . . . . . . . .           |
|          . . . . . . . . . .          |
|         . . . . . . . . . . .         |
|        . . . . . . . . . . . .        |
|       . . . . . . . . . . . . .       |
|      . . . . . . . . . . . . . .      |
|     . . . . . . . . . . . . . . .     |
|    . . . . . . . . . . . . . . . .    |
|   . . . . . . . . . . . . . . . . .   |
|  . . . . . . . . . . . . . . . . . .  |
| . . . . . . . . . . . . . . . . . . . |
|                                       |
|                                       |
|                  o                    |
|                  o                    |
|                o o                    |
|                o o                    |
|                o o o                  |
|                o o o                  |
|                o o o                  |
|                o o o                  |
|                o o o o                |
|                o o o o o              |
|          o     o o o o o o            |
|          o o o o o o o o o            |
|        o o o o o o o o o o o o        |
+---------------------------------------+

Liberty BASIC

User can choose the number of balls to run through the simulation.

[setup]
    nomainwin

    speed=50

    prompt "Number of balls to drop: ";cycleMax
    cycleMax=abs(int(cycleMax))

'create window
    WindowWidth=400
    WindowHeight=470
    UpperLeftX=1
    UpperLeftY=1
    graphicbox #1.gb, 10, 410,370,25
    open "Galton Machine" for graphics_nf_nsb as #1
    #1 "trapclose [q];down;fill black;flush"
    #1.gb "font courier_new 12"

'Create graphical sprites
    #1 "getbmp bg 1 1 400 600"
    #1 "place 0 0; color white;backcolor white;boxfilled 17 17;place 8 8;color black;backcolor black;circlefilled 8;"
    #1 "place 8 25;color white;backcolor white;circlefilled 8;"
    #1 "getbmp ball 0 0 17 34"
    #1 "place 8 25;color red;backcolor red;circlefilled 8;"
    #1 "getbmp pin 0 0 17 34"
    #1 "background bg"

'add sprites to program
    for pinCount = 1 to 28
        #1 "addsprite pin";pinCount;" pin;spriteround pin";pinCount
    next pinCount

    for ballCount = 1 to 7
        #1 "addsprite ball";ballCount;" ball;spriteround ball";ballCount
    next ballCount

'place pins on page
    for y = 1 to 7
        for x = 1 to y
            pin=pin+1
            xp=200-x*50+y*25
            yp=y*35+100
            #1 "spritexy pin";pin;" ";xp;" ";yp
            #1 "drawsprites"
        next x
    next y

'set balls in motion
    for a = 1 to 7
        #1 "spritexy ball";a;" 174 ";a*60-350
        #1 "spritemovexy ball";a;" 0 5"
    next a

[start] 'update every 50ms - lower number means faster updates
    timer speed, [move]
    wait

[move] 'cycle through the sprites to check for contact with pins or dropping off board
    #1 "drawsprites"
    for ballNum = 1 to 7
        gosub [checkCollide]
    next ballNum
    timer speed, [move]
    wait

[checkCollide] 'check for contact with pins or dropping off board
    timer 0
    #1 "spritexy? ball";ballNum;" x y" 'get current ball position
    #1 "spritecollides ball";ballNum;" hits$" 'collect any collisions
    if hits$<>"" then 'any collisions? if so...
        direction = rnd(1)
        'randomly bounce either left or right
        if direction >0.4999999 then #1 "spritexy ball";ballNum;" ";x+25;" ";y else #1 "spritexy ball";ballNum;" ";x-25;" ";y
        #1 "spritemovexy ball";ballNum;" 0 5"'set ball in motion again
    end if
    #1 "spritexy? ball";ballNum;" x y" 'get current ball position
    if y > 400 then 'if ball has dropped off board, then...
        select case 'figure out which slot it has landed in and increment the counter for that slot
            case x<49
                slot(1)=slot(1)+1
            case x=49
                slot(2)=slot(2)+1
            case x=99
                slot(3)=slot(3)+1
            case x=149
                slot(4)=slot(4)+1
            case x=199
                slot(5)=slot(5)+1
            case x=249
                slot(6)=slot(6)+1
            case x=299
                slot(7)=slot(7)+1
            case x>299
                slot(8)=slot(8)+1
        end select
        for a = 1 to 8 'write the slot counts in the small graphic box
            update$="place "+str$((a-1)*48+1)+" 20;\"+str$(slot(a))
            #1.gb, update$
        next a
        #1 "spritexy ball";ballNum;" 174 ";0-10 'reposition the sprite just off the top of the screen
        #1 "spritemovexy ball";ballNum;" 0 5" 'set the ball in motion again
        cycles = cycles + 1 'increment the fallen ball count
        if cycles >= cycleMax then
            timer 0 'stop animation
            'make the visible balls go away
            for a = 1 to 7
                #1 "spritexy ball";a;" 174 700"
                #1 "spritemovexy ball";a;" 0 0"
            next a
            #1 "drawsprites"
            notice "Complete"
            wait
        end if
    end if
return

[q]
    close #1
    'It is IMPORTANT to unload the bitmaps and clear memory
    unloadbmp "pin"
    unloadbmp "ball"
    unloadbmp "bg"
    end

Lua

Uses Bitmap class here, with an ASCII pixel representation, then extending..

Bitmap.render = function(self)
  for y = 1, self.height do
    print(table.concat(self.pixels[y], " "))
  end
end

-- globals (tweak here as desired)
math.randomseed(os.time())
local W, H, MIDX = 15, 40, 7
local bitmap = Bitmap(W, H)
local AIR, PIN, BALL, FLOOR = ".", "▲", "☻", "■"
local nballs, balls = 60, {}
local frame, showEveryFrame = 1, false

-- the game board:
bitmap:clear(AIR)
for row = 1, 7 do
  for col = 0, row-1 do
    bitmap:set(MIDX-row+col*2+1, 1+row*2, PIN)
  end
end
for col = 0, W-1 do
  bitmap:set(col, H-1, FLOOR)
end

-- ball class
Ball = {
  new = function(self, x, y, bitmap)
    local instance = setmetatable({ x=x, y=y, bitmap=bitmap, alive=true }, self)
    return instance
  end,
  update = function(self)
    if not self.alive then return end
    self.bitmap:set(self.x, self.y, AIR)
    local newx, newy = self.x, self.y+1
    local below = self.bitmap:get(newx, newy)
    if below==PIN then
      newx = newx + (math.random(2)-1)*2-1
    end
    local there = self.bitmap:get(newx, newy)
    if there==AIR then
      self.x, self.y = newx, newy
    else
      self.alive = false
    end
    self.bitmap:set(self.x, self.y, BALL)
  end,
}
Ball.__index = Ball
setmetatable(Ball, { __call = function (t, ...) return t:new(...) end })

-- simulation:
local function spawn()
  if nballs > 0 then
    balls[#balls+1] = Ball(MIDX, 0, bitmap)
    nballs = nballs - 1
  end
end

spawn()
while #balls > 0 do
  if frame%2==0 then spawn() end
  alive = {}
  for _,ball in ipairs(balls) do
    ball:update()
    if ball.alive then alive[#alive+1]=ball end
  end
  balls = alive
  if frame%50==0 or #alive==0 or showEveryFrame then
    print("FRAME "..frame..":")
    bitmap:render()
  end
  frame = frame + 1
end
Output:
FRAME 50:
. . . . . . . . . . . . . . .
. . . . . . . ☻ . . . . . . .
. . . . . . . . . . . . . . .
. . . . . . ☻ ▲ . . . . . . .
. . . . . . . . . . . . . . .
. . . . . ☻ ▲ . ▲ . . . . . .
. . . . . . . . . . . . . . .
. . . . . ▲ ☻ ▲ . ▲ . . . . .
. . . . . . . . . . . . . . .
. . . . ▲ . ▲ . ▲ ☻ ▲ . . . .
. . . . . . . . . . . . . . .
. . . ▲ . ▲ ☻ ▲ . ▲ . ▲ . . .
. . . . . . . . . . . . . . .
. . ▲ . ▲ . ▲ ☻ ▲ . ▲ . ▲ . .
. . . . . . . . . . . . . . .
. ▲ . ▲ . ▲ . ▲ ☻ ▲ . ▲ . ▲ .
. . . . . . . . . . . . . . .
. . . . ☻ . . . . . . . . . .
. . . . . . . . . . . . . . .
. . . . . . ☻ . . . . . . . .
. . . . . . . . . . . . . . .
. . . . . . . . . . ☻ . . . .
. . . . . . . . . . . . . . .
. . . . . . . . . . ☻ . . . .
. . . . . . . . . . . . . . .
. . . . . . ☻ . . . . . . . .
. . . . . . . . . . . . . . .
. . . . ☻ . . . . . . . . . .
. . . . . . . . . . . . . . .
. . . . ☻ . . . . . . . . . .
. . . . . . . . . . . . . . .
. . . . . . ☻ . . . . . . . .
. . . . . . . . . . . . . . .
. . . . . . . . . . . . . . ☻
. . . . . . . . . . . . . . .
. . . . . . ☻ . . . . . . . .
. . . . . . ☻ . . . . . . . .
. . ☻ . . . ☻ . . . ☻ . . . .
. . ☻ . . . ☻ . ☻ . ☻ . . . .
■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■
FRAME 100:
. . . . . . . . . . . . . . .
. . . . . . . ☻ . . . . . . .
. . . . . . . . . . . . . . .
. . . . . . ☻ ▲ . . . . . . .
. . . . . . . . . . . . . . .
. . . . . . ▲ ☻ ▲ . . . . . .
. . . . . . . . . . . . . . .
. . . . . ▲ ☻ ▲ . ▲ . . . . .
. . . . . . . . . . . . . . .
. . . . ▲ . ▲ . ▲ ☻ ▲ . . . .
. . . . . . . . . . . . . . .
. . . ▲ . ▲ . ▲ ☻ ▲ . ▲ . . .
. . . . . . . . . . . . . . .
. . ▲ . ▲ ☻ ▲ . ▲ . ▲ . ▲ . .
. . . . . . . . . . . . . . .
. ▲ ☻ ▲ . ▲ . ▲ . ▲ . ▲ . ▲ .
. . . . . . . . . . . . . . .
. . . . . . ☻ . . . . . . . .
. . . . . . . . . . . . . . .
. . . . . . ☻ . . . . . . . .
. . . . . . . . . . . . . . .
. . . . ☻ . . . . . . . . . .
. . . . . . . . . . . . . . .
. . . . . . . . ☻ . . . . . .
. . . . . . . . . . . . . . .
. . . . . . ☻ . ☻ . . . . . .
. . . . . . ☻ . . . . . . . .
. . . . ☻ . ☻ . . . . . . . .
. . . . . . ☻ . . . . . . . .
. . . . . . ☻ . ☻ . . . . . .
. . . . . . ☻ . . . . . . . .
. . . . ☻ . ☻ . . . . . . . .
. . . . . . ☻ . ☻ . . . . . .
. . . . . . ☻ . ☻ . . . . . .
. . . . . . ☻ . ☻ . ☻ . . . .
. . . . ☻ . ☻ . ☻ . ☻ . . . .
. . ☻ . ☻ . ☻ . ☻ . ☻ . . . .
. . ☻ . ☻ . ☻ . ☻ . ☻ . . . .
☻ . ☻ . ☻ . ☻ . ☻ . ☻ . . . ☻
■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■
FRAME 147:
. . . . . . . . . . . . . . .
. . . . . . . . . . . . . . .
. . . . . . . . . . . . . . .
. . . . . . . ▲ . . . . . . .
. . . . . . . . . . . . . . .
. . . . . . ▲ . ▲ . . . . . .
. . . . . . . . . . . . . . .
. . . . . ▲ . ▲ . ▲ . . . . .
. . . . . . . . . . . . . . .
. . . . ▲ . ▲ . ▲ . ▲ . . . .
. . . . . . . . . . . . . . .
. . . ▲ . ▲ . ▲ . ▲ . ▲ . . .
. . . . . . . . . . . . . . .
. . ▲ . ▲ . ▲ . ▲ . ▲ . ▲ . .
. . . . . . . . . . . . . . .
. ▲ . ▲ . ▲ . ▲ . ▲ . ▲ . ▲ .
. . . . . . ☻ . . . . . . . .
. . . . . . ☻ . . . . . . . .
. . . . . . ☻ . . . . . . . .
. . . . . . ☻ . . . . . . . .
. . . . . . ☻ . . . . . . . .
. . . . . . ☻ . . . . . . . .
. . . . . . ☻ . . . . . . . .
. . . . . . ☻ . . . . . . . .
. . . . . . ☻ . . . . . . . .
. . . . . . ☻ . ☻ . . . . . .
. . . . . . ☻ . ☻ . . . . . .
. . . . . . ☻ . ☻ . . . . . .
. . . . . . ☻ . ☻ . . . . . .
. . . . . . ☻ . ☻ . . . . . .
. . . . ☻ . ☻ . ☻ . . . . . .
. . . . ☻ . ☻ . ☻ . . . . . .
. . . . ☻ . ☻ . ☻ . . . . . .
. . . . ☻ . ☻ . ☻ . ☻ . . . .
. . ☻ . ☻ . ☻ . ☻ . ☻ . . . .
. . ☻ . ☻ . ☻ . ☻ . ☻ . . . .
. . ☻ . ☻ . ☻ . ☻ . ☻ . . . .
. . ☻ . ☻ . ☻ . ☻ . ☻ . . . .
☻ . ☻ . ☻ . ☻ . ☻ . ☻ . ☻ . ☻
■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■

Mathematica / Wolfram Language

ClearAll[MakePathFunction]
MakePathFunction[{path_, acumpath_}] := 
 Module[{f1, f2, f3, pf, n = Length[path]},
  f1 = MapThread[{#1/2, #2 + 0.5 < z <= #2 + 1} &, {acumpath, 
     n - Range[n + 1]}];
  f2 = MapThread[{#1/2 + #2 Sqrt[1/4 - (z - #3)^2], #3 < 
       z <= #3 + 1/2} &, {acumpath // Most, path, n - Range[n]}];
  f3 = {{acumpath[[-1]]/2, z <= 0}};
  pf = Piecewise[Evaluate[Join[f1, f2, f3]], 0];
  pf
  ]
MakeScene[pfs_List, zfinals_List, n_Integer, t_] := 
 Module[{durations, accumduration, if, part, fixed, relt},
  durations = n - zfinals;
  accumduration = Accumulate[Prepend[durations, 0]];
  if = Interpolation[{accumduration, Range[Length[zfinals] + 1]} // 
     Transpose, InterpolationOrder -> 1];
  part = Floor[if[t]];
  If[part > 0,
   fixed = Table[{pfs[[i]], z} /. z -> zfinals[[i]], {i, part - 1}];
   ,
   fixed = {};
   ];
  relt = t - accumduration[[part]];
  relt = n - relt;
  Append[fixed, {pfs[[part]] /. z -> relt, relt}]
  ]
SeedRandom[1234];
n = 6;
m = 150;
r = 0.25; (* fixed *)
dots = 
 Catenate@Table[{# - i/2 - 1/2, n - i} & /@ Range[i], {i, n}];
g = Graphics[Disk[#, r] & /@ dots, Axes -> True];

paths = RandomChoice[{-1, 1}, {m, n}];
paths = {#, Accumulate[Prepend[#, 0]]} & /@ paths;
xfinals = paths[[All, 2, -1]];
types = DeleteDuplicates[xfinals];
zfinals = ConstantArray[0, Length[paths]];
Do[
  pos = Flatten[Position[xfinals, t]];
  zfinals[[pos]] += 0.5 Range[Length[pos]];
  ,
  {t, types}
  ];
max = Max[zfinals] + 1;
zfinals -= max;
pfs = MakePathFunction /@ paths;
Manipulate[
 Graphics[{Disk[#, r] & /@ dots, Red, 
   Disk[#, r] & /@ MakeScene[pfs, zfinals, n, t]}, 
  PlotRange -> {{-n, n}, {Min[zfinals] - 1, n + 2}}, 
  ImageSize -> 150], {t, 0, Total[n - zfinals] - 0.001}]

Nim

Translation of: Go
import random, strutils

const
  BoxW = 41         # Galton box width.
  BoxH = 37         # Galton box height.
  PinsBaseW = 19    # Pins triangle base.
  NMaxBalls = 55    # Number of balls.

const CenterH = PinsBaseW + (BoxW - (PinsBaseW * 2 - 1)) div 2 - 1

type

  Cell = enum
    cEmpty = " "
    cBall = "o"
    cWall = "|"
    cCorner = "+"
    cFloor = "-"
    cPin = "."

  # Galton box. Will be printed upside-down.
  Box = array[BoxH, array[BoxW, Cell]]

  Ball = ref object
    x, y: int


func initBox(): Box =

  # Set ceiling and floor.
  result[0][0] = cCorner
  result[0][^1] = cCorner
  for i in 1..(BoxW - 2):
    result[0][i] = cFloor
  result[^1] = result[0]

  # Set walls.
  for i in 1..(BoxH - 2):
    result[i][0] = cWall
    result[i][^1] = cWall

  # Set rest to Empty initially.
  for i in 1..(BoxH - 2):
    for j in 1..(BoxW - 2):
      result[i][j] = cEmpty

  # Set pins.
  for nPins in 1..PinsBaseW:
    for p in 0..<nPins:
      result[BoxH - 2 - nPins][CenterH + 1 - nPins + p * 2] = cPin


func newBall(box: var Box; x, y: int): Ball =

  doAssert box[y][x] == cEmpty, "Tried to create a new ball in a non-empty cell"
  result = Ball(x: x, y: y)
  box[y][x] = cBall


proc doStep(box: var Box; b: Ball) =

  if b.y <= 0:
    return    # Reached the bottom of the box.

  case box[b.y-1][b.x]

  of cEmpty:
    box[b.y][b.x] = cEmpty
    dec b.y
    box[b.y][b.x] = cBall

  of cPin:
    box[b.y][b.x] = cEmpty
    dec b.y
    if box[b.y][b.x-1] == cEmpty and box[b.y][b.x+1] == cEmpty:
      inc b.x, 2 * rand(1) - 1
    elif box[b.y][b.x-1] == cEmpty:
      inc b.x
    else:
      dec b.x
    box[b.y][b.x] = cBall

  else:
    # It's frozen - it always piles on other balls.
    discard


proc draw(box: Box) =
  for r in countdown(BoxH - 1, 0):
    echo box[r].join()


#———————————————————————————————————————————————————————————————————————————————————————————————————

randomize()
var box = initBox()
var balls: seq[Ball]

for i in 0..<(NMaxBalls + BoxH):

  echo "Step ", i, ':'
  if i < NMaxBalls:
    balls.add box.newBall(CenterH, BoxH - 2)
  box.draw()

  # Next step for the simulation.
  # Frozen balls are kept in balls slice for simplicity.
  for ball in balls:
    box.doStep(ball)
Output:

Sample output (showing last step only):

Step 91:
+---------------------------------------+
|                                       |
|                   .                   |
|                  . .                  |
|                 . . .                 |
|                . . . .                |
|               . . . . .               |
|              . . . . . .              |
|             . . . . . . .             |
|            . . . . . . . .            |
|           . . . . . . . . .           |
|          . . . . . . . . . .          |
|         . . . . . . . . . . .         |
|        . . . . . . . . . . . .        |
|       . . . . . . . . . . . . .       |
|      . . . . . . . . . . . . . .      |
|     . . . . . . . . . . . . . . .     |
|    . . . . . . . . . . . . . . . .    |
|   . . . . . . . . . . . . . . . . .   |
|  . . . . . . . . . . . . . . . . . .  |
| . . . . . . . . . . . . . . . . . . . |
|                                       |
|                                       |
|                                       |
|                      o                |
|                      o                |
|                      o                |
|                o     o                |
|                o     o                |
|                o o   o                |
|              o o o   o o              |
|              o o o o o o o            |
|              o o o o o o o            |
|            o o o o o o o o            |
|            o o o o o o o o o          |
|            o o o o o o o o o          |
+---------------------------------------+

Perl

translated from Raku

Output shows of final state for a run with 50 coins.

Translation of: Raku
use strict;
use warnings;

use List::Util 'any';
use Time::HiRes qw(sleep);
use List::AllUtils <pairwise pairs>;

use utf8;
binmode STDOUT, ':utf8';

my $coins      = shift || 100;
my $peg_lines  = shift || 13;
my $row_count  = $peg_lines;
my $peg        = '^';
my @coin_icons = ("\N{UPPER HALF BLOCK}", "\N{LOWER HALF BLOCK}");

my @coins = (undef) x (3 + $row_count + 4);
my @stats = (0) x ($row_count * 2);
$coins[0] = 0; # initialize with first coin

while (1) {
    my $active = 0;
    # if a coin falls through the bottom, count it
    $stats[$coins[-1] + $row_count]++ if defined $coins[-1];

    # move every coin down one row
    for my $line (reverse 1..(3+$row_count+3) ) {
        my $coinpos = $coins[$line - 1];

        #$coins[$line] = do if (! defined $coinpos) x
        if (! defined $coinpos) {
            $coins[$line] = undef
        } elsif (hits_peg($coinpos, $line)) {
            # when a coin from above hits a peg, it will bounce to either side.
            $active = 1;
            $coinpos += rand() < .5 ? -1 : 1;
            $coins[$line] = $coinpos
        } else {
            # if there was a coin above, it will fall to this position.
            $active = 1;
            $coins[$line] = $coinpos;
        }
    }
    # let the coin dispenser blink and turn it off if we run out of coins
    if (defined $coins[0]) {
        $coins[0] = undef;
    } elsif (--$coins > 0) {
        $coins[0] = 0
    }

    for (<0 1>) {
        display_board(\@coins, \@stats, $_);
        sleep .1;
    }
    exit unless $active;
}

sub display_board {
    my($p_ref, $s_ref, $halfstep) = @_;
    my @positions = @$p_ref;
    my @stats     = @$s_ref;
    my $coin      = $coin_icons[$halfstep];

    my @board = do {
        my @tmpl;

        sub out {
            my(@stuff) = split '', shift;
            my @line;
            push @line, ord($_) for @stuff;
            [@line];
        }

        push @tmpl, out("  " . " "x(2 * $row_count)) for 1..3;
        my @a = reverse 1..$row_count;
        my @b = 1..$row_count;
        my @pairs = pairwise { ($a, $b) } @a, @b;
        for ( pairs @pairs ) {
            my ( $spaces, $pegs ) = @$_;
            push @tmpl, out("  " . " "x$spaces . join(' ',($peg) x $pegs) . " "x$spaces);
        }
        push @tmpl, out("  " . " "x(2 * $row_count)) for 1..4;
        @tmpl;
    };

    my $midpos = $row_count + 2;

    our @output;
    {
        # collect all the output and output it all at once at the end
        sub printnl { my($foo) = @_; push @output, $foo . "\n" }
        sub printl  { my($foo) = @_; push @output, $foo        }

        # make some space above the picture
        printnl("") for 0..9;

        # place the coins
        for my $line (0..$#positions) {
            my $pos = $positions[$line];
            next unless defined $pos;
            $board[$line][$pos + $midpos] = ord($coin);
        }
        # output the board with its coins
        for my $line (@board) {
            printnl join '', map { chr($_) } @$line;
        }

        # show the statistics
        my $padding = 0;
        while (any {$_> 0} @stats) {
            $padding++;
            printl "  ";
            for my $i (0..$#stats) {
                if ($stats[$i] == 1) {
                        printl "\N{UPPER HALF BLOCK}";
                        $stats[$i]--;
                } elsif ($stats[$i] <= 0) {
                        printl " ";
                        $stats[$i] = 0
                } else {
                        printl "\N{FULL BLOCK}";
                        $stats[$i]--; $stats[$i]--;
                }
            }
            printnl("");
        }
        printnl("") for $padding..(10-1);
    }

    print join('', @output) . "\n";
}

sub hits_peg {
    my($x, $y) = @_;
    3 <= $y && $y < (3 + $row_count) and -($y - 2) <= $x && $x <= $y - 2
        ? not 0 == ($x - $y) % 2
        : 0
}
Output:
         ^
        ^ ^
       ^ ^ ^
      ^ ^ ^ ^
     ^ ^ ^ ^ ^
    ^ ^ ^ ^ ^ ^
   ^ ^ ^ ^ ^ ^ ^




    █ █ █ █ █ █
      █ █ █ █
      █ █ █ █
      █ █ █ █
      █ █ █ ▀
      ▀ █ █
        █

native Perl

Runs until a bottom column overflows.

#!/usr/bin/perl

use strict; # https://rosettacode.org/wiki/Galton_box_animation
use warnings;
$| = 1;

my $width = shift // 7;
my $bottom = 15;
my $blank = ' ' x ( 2 * $width + 1 ) . "\n";
my $line = ' ' x $width . '*' . ' ' x $width . "\n";
local $_ = join '', $blank x 5 . $line,
  map({ $line =~ s/ \* (.*) /* * $1/; ($blank, $line) } 2 .. $width ),
  $blank x $bottom;

my $gap = / \n/ && $-[0];
my $gl = qr/.{$gap}/s;
my $g = qr/.$gl/s;
my $center = $gap >> 1;
my %path = ('O* ' => 'O*X', ' *O' => 'X*O');

print "\e[H\e[J$_";
while( not /(?:O$g){$bottom}/ )
  {
  my $changes = s!O($gl)( \* |O\* | \*O)! " $1" .
    ($path{$2} // (rand(2) < 1 ? "X* " : " *X")) !ge + 
    s/O($g) / $1X/g + 
    s/^ {$center}\K ($g $g) /O$1 /;
  tr/X/O/;
  print "\e[H$_";
  $changes or last;
  select undef, undef, undef, 0.05;
  }
Output:
               
               
       O       
               
               
       *O      
               
      * *      
     O         
     * * *     
               
    *O* * *    
               
   * * * * *   
    O          
  * * * * * *  
               
 * * *O* * * * 
      O        
      O        
      O   O    
      O        
      O        
      O        
      O        
      O O      
      O O      
    O O O      
    O O O O    
    O O O O    
    O O O O    
    O O O O    
  O O O O O O

Phix

console

First, a console version:

without js -- clear_screen(), text_color(), position(), sleep(), get_key()...
constant balls = 80
clear_screen()
sequence screen = repeat(repeat(' ',23),12)
                & repeat(join(repeat(':',12)),12)
                & {repeat('.',23)},
         Pxy = repeat({12,1},balls)
for peg=1 to 10 do
    screen[peg+2][13-peg..11+peg] = join(repeat('.',peg))
end for
puts(1,join(screen,"\n"))
text_color(BRIGHT_RED)
bool moved = true
integer top = ' '   -- (new drop every other iteration)
while moved or top!=' ' do
    moved = false
    for i=1 to balls do
        integer {Px,Py} = Pxy[i]
        if Py!=1 or top=' ' then
            integer Dx = 0, Dy = 0
            if screen[Py+1,Px]=' ' then     -- can vertical?
                Dy = 1
            else
                Dx = {-1,+1}[rand(2)]       -- try l;r or r;l
                if screen[Py+1,Px+Dx]!=' ' then Dx = -Dx end if
                if screen[Py+1,Px+Dx]==' ' then 
                    Dy = 1
                end if
            end if
            if Dy then
                position(Py,Px)  puts(1," ")  screen[Py,Px] = ' '
                Px += Dx
                Py += Dy
                position(Py,Px)  puts(1,"o")  screen[Py,Px] = 'o'
                Pxy[i] = {Px,Py}
                moved = true
                if Py=2 then top = 'o' end if
            end if
        end if
    end for
    position(26,1)
    sleep(0.2)
    if get_key()!=-1 then exit end if
    top = screen[2][12]
end while
Output:
           .o
          . .
         .o. .
        . . . .
       . .o. . .
      . . . . . .
     . . . . .o. .
    . . . . . . . .
   . . . . .o. . . .
  . . . . . . . . . .
: : : : :o: : : : : : :
: : : : : : : : : : : :
: : : : : : : : :o: : :
: : : : : : : : : : : :
: : : : : : : :o: : : :
: : : : : : : : : : : :
: : : : :o:o:o: : : : :
: : : : :o:o:o: : : : :
: : :o: :o:o:o: : : : :
: : : : :o:o:o:o: : : :
: : : :o:o:o:o:o: : : :
: : :o:o:o:o:o:o: : : :
.......................

gui

Also, here is a slightly nicer and resize-able gui version:

Library: Phix/pGUI
Library: Phix/online

You can run this online here.

--
-- demo\rosetta\GaltonBox.exw
-- ==========================
--
-- Author Pete Lomax, May 2017
--
with javascript_semantics
constant TITLE = "Galton Box"

include pGUI.e

Ihandle dlg, canvas, timershow
cdCanvas cddbuffer, cdcanvas

integer brem = 80
sequence balls = {{0,1,0}}
sequence bins = repeat(0,8)

function redraw_cb(Ihandle /*ih*/, integer /*posx*/, /*posy*/)
    integer {w, h} = IupGetIntInt(canvas, "DRAWSIZE"), x, y
    atom xx, yy
    cdCanvasActivate(cddbuffer)
    cdCanvasClear(cddbuffer)
    -- draw the pins, then balls, then bins
    cdCanvasSetForeground(cddbuffer, CD_DARK_GREEN)
    integer pinsize = min(floor(h/40),floor(w/50))
    for y=4 to 16 by 2 do
        for x=-(y-4) to (y-4) by 4 do
            xx = w/2 + x*w/32
            yy = h -y*h/32
            cdCanvasSector(cddbuffer, xx, yy, pinsize, pinsize, 0, 360) 
        end for
    end for
    cdCanvasSetForeground(cddbuffer, CD_INDIGO)
    for i=1 to length(balls) do
        {x, y} = balls[i]
        xx = w/2 + x*w/32
        yy = h -y*h/32
        cdCanvasSector(cddbuffer, xx, yy, pinsize*4, pinsize*4, 0, 360) 
    end for
    cdCanvasSetLineWidth(cddbuffer,w/9)
    for i=1 to length(bins) do
        xx = w/2+(i*4-18)*w/32
        yy = bins[i]*h/64+10
        cdCanvasLine(cddbuffer,xx,10,xx,yy)
    end for
    cdCanvasFlush(cddbuffer)
    return IUP_DEFAULT
end function

function timer_cb(Ihandle ih)
    integer x, y=9, dx
    if length(balls) then
        {x,y,dx} = balls[1]
        if y>20 then
            integer bindx = (x+18)/4
            bins[bindx] += 1
            balls = balls[2..$]
        end if
    end if
    for i=1 to length(balls) do
        {x,y,dx} = balls[i]
        if y>15 then
            dx = 0
        elsif and_bits(y,1)=0 then
            dx = {-1,+1}[rand(2)]
        end if
        balls[i] = {x+dx,y+1,dx}
    end for
    if y>4 and brem!=0 then
        brem -= 1
        balls = append(balls,{0,1,0})
    end if
    if brem=0 and length(balls)=0 then
        IupSetAttribute(timershow,"RUN","NO")
    end if
    IupUpdate(canvas)
    return IUP_IGNORE
end function

function map_cb(Ihandle ih)
    cdcanvas = cdCreateCanvas(CD_IUP, ih)
    cddbuffer = cdCreateCanvas(CD_DBUFFER, cdcanvas)
    cdCanvasSetBackground(cddbuffer, CD_GREY)
    return IUP_DEFAULT
end function

procedure main()
    IupOpen()

    canvas = IupCanvas("RASTERSIZE=360x600")
    IupSetCallbacks(canvas, {"MAP_CB", Icallback("map_cb"),
                             "ACTION", Icallback("redraw_cb")})

    timershow = IupTimer(Icallback("timer_cb"), 80)

    dlg = IupDialog(canvas, `TITLE="%s"`, {TITLE})

    IupShow(dlg)
    IupSetAttribute(canvas, "RASTERSIZE", NULL)
    if platform()!=JS then
        IupMainLoop()
        IupClose()
    end if
end procedure

main()

PicoLisp

(de galtonBox (Pins Height)
   (let (Bins (need (inc (* 2 Pins)) 0)  X 0  Y 0)
      (until (= Height (apply max Bins))
         (call 'clear)
         (cond
            ((=0 Y) (setq X (inc Pins)  Y 1))
            ((> (inc 'Y) Pins)
               (inc (nth Bins X))
               (zero Y) ) )
         ((if (rand T) inc dec) 'X)
         (for Row Pins
            (for Col (+ Pins Row 1)
               (let D (dec (- Col (- Pins Row)))
                  (prin
                     (cond
                        ((and (= X Col) (= Y Row)) "o")
                        ((and (gt0 D) (bit? 1 D)) ".")
                        (T " ") ) ) ) )
            (prinl) )
         (prinl)
         (for H (range Height 1)
            (for B Bins
               (prin (if (>= B H) "o" " ")) )
            (prinl) )
         (wait 200) ) ) )

Test:

(galtonBox 9 11)
Output:
# Snapshot after a few seconds:
         .
        . .
       . . .
      . . . .
     . . . . .
    .o. . . . .
   . . . . . . .
  . . . . . . . .
 . . . . . . . . .









        o
        o o
      o o o o
# Final state:
         .
        . .
       . . .
      . . . .
     . . . . .
    . . . . . .
   . . . . . . .
  . . . . . . . .
 . . . . . . . . .

        o
        o
        o
        o
        o
      o o
      o o
      o o
      o o o
      o o o o o
    o o o o o o

Prolog

Works with SWI-Prolog and XPCE.

Sample display of Prolog solution
:- dynamic tubes/1.
:- dynamic balls/2.
:- dynamic stop/1.

% number of rows of pins (0 -> 9)
row(9).

galton_box :-
	retractall(tubes(_)),
	retractall(balls(_,_)),
	retractall(stop(_)),
	assert(stop(@off)),
	new(D, window('Galton Box')),
	send(D, size, size(520,700)),
	display_pins(D),
	new(ChTubes, chain),
	assert(tubes(ChTubes)),
	display_tubes(D, ChTubes),
	new(Balls, chain),
	new(B, ball(D)),
	send(Balls, append, B),
	assert(balls(Balls, D)),
	send(D, done_message, and(message(ChTubes, clear),
				  message(ChTubes, free),
				  message(Balls, for_all, message(@arg1, free)),
				  message(Balls, clear),
				  message(Balls, free),
				  message(@receiver, destroy))),
	send(D, open).

% class pin, balls travel between pins
:- pce_begin_class(pin, circle, "pin").

initialise(P, Pos) :->
	send(P, send_super, initialise, 18),
	send(P, fill_pattern, new(_, colour(@default, 0, 0, 0))),
	get(Pos, x, X),
	get(Pos, y, Y),
	send(P, geometry, x := X, y := Y).
:- pce_end_class.


% class tube, balls fall in them
:- pce_begin_class(tube, path, "tube where balls fall").

variable(indice, any, both, "index of the tube in the list").
variable(balls, any, both, "number of balls inside").

initialise(P, Ind, D) :->
	row(Row),
	send(P, send_super, initialise, kind := poly),
	send(P, slot, balls, 0),
	send(P, slot, indice, Ind),
	X0 is 228 - Row * 20 + Ind * 40,
	X1 is X0 + 20,
	Y1 is 600, Y0 is 350,
	send_list(P, append, [point(X0, Y0), point(X0, Y1),
			      point(X1,Y1), point(X1,Y0)]),
	send(D, display, P).

% animation stop when a tube is full
add_ball(P) :->
	get(P, slot, balls, B),
	B1 is B+1,
	send(P, slot, balls, B1),
	(   B1 = 12
	->  retract(stop(_)), assert(stop(@on))
	;   true).
:- pce_end_class.


% class ball
:- pce_begin_class(ball, circle, "ball").

variable(angle, any, both, "angle of the ball with the pin").
variable(dir, any, both, "left / right").
variable(pin, point, both, "pin under the ball when it falls").
variable(position, point, both, "position of the ball").
variable(max_descent, any, both, "max descent").
variable(state, any, both, "in_pins / in_tube").
variable(window, any, both, "window to display").
variable(mytimer, timer, both, "timer of the animation").

initialise(P, W) :->
	send(P, send_super, initialise, 18),
	send(P, pen, 0),
	send(P, state, in_pins),
	send(P, fill_pattern, new(_, colour(@default, 65535, 0, 0))),
	Ang is 3 * pi / 2,
	send(P, slot, angle, Ang),
	send(P, slot, window, W),
	send(P, geometry, x := 250, y := 30),
	send(P, pin, point(250, 50)),
	send(P, choose_dir),
	send(P, mytimer, new(_, timer(0.005, message(P, move_ball)))),
	send(W, display, P),
	send(P?mytimer, start).

% method called when the object is destroyed
% first the timer is stopped
% then all the resources are freed
unlink(P) :->
	send(P?mytimer, stop),
	send(P, send_super, unlink).

choose_dir(P) :->
	I is random(2),
	(   I = 1 -> Dir = left; Dir = right),
	send(P, dir, Dir).

move_ball(P) :->
	get(P, state, State),
	(   State = in_pins
	->  send(P, move_ball_in_pins)
	;   send(P, move_ball_in_tube)).

move_ball_in_pins(P) :->
	get(P, slot, angle, Ang),
	get(P, slot, pin, Pin),
	get(P, slot, dir, Dir),
	(   Dir = left -> Ang1 is Ang-0.15 ; Ang1 is Ang + 0.15),
	get(Pin, x, PX),
	get(Pin, y, PY),
	X is 21 * cos(Ang1) +  PX,
	Y is 21 * sin(Ang1) + PY,
	send(P, geometry, x := X, y := Y),
	send(P?window, display, P),
	(   abs(Ang1 - pi) < 0.1
	->  PX1 is PX - 20,
	    send(P, next_move, PX1, PY)
	;   abs(Ang1 - 2 * pi) < 0.1
	->  PX1 is PX + 20,
	    send(P, next_move, PX1, PY)
	;   send(P, slot, angle, Ang1)).

next_move(P, PX, PY) :->
	row(Row),

	    Ang2 is 3 * pi / 2,
	    PY1 is PY + 30,
	    (	PY1 =:= (Row + 1) * 30 + 50
	    ->	send(P, slot, state, in_tube),
		NumTube is round((PX - 228 + Row * 20) / 40),
		tubes(ChTubes),
		get(ChTubes, find,
		    message(@prolog, same_value,@arg1?indice, NumTube),
		    Tube),
		send(Tube, add_ball),
		get(Tube, slot, balls, Balls),
		Max_descent is 600 - Balls * 20,
		send(P, slot, max_descent, Max_descent),
		send(P, slot, position, point(PX, PY))
	    ;	send(P, choose_dir),
		send(P, slot, angle, Ang2),
		send(P, slot, pin, point(PX, PY1))).

move_ball_in_tube(P) :->
	get(P, slot, position, Descente),
	get(Descente, x, PX1),
	get(Descente, y, PY),
	PY1 is PY+4,
	send(P, geometry, x := PX1, y := PY1),
	get(P, slot, max_descent, Max_descent),
	(   Max_descent =< PY1
	->  send(P?mytimer, stop),
	    (	stop(@off) ->  send(@prolog, next_ball); true)
	;   send(P, slot, position, point(PX1, PY1))),
	send(P?window, display, P).

:- pce_end_class.


next_ball :-
	retract(balls(Balls, D)),
	new(B, ball(D)),
	send(Balls, append, B),
	assert(balls(Balls, D)).

% test to find the appropriate tube
same_value(V, V).

display_pins(D) :-
	row(Row),
	forall(between(0, Row, I),
	       (  Start is 250 - I * 20,
		  Y is I * 30 + 50,
	          forall(between(0, I, J),
			 (   X is Start + J * 40,
			     new(P, pin(point(X,Y))),
			     send(D, display, P))))).

display_tubes(D, Ch) :-
	row(Row),
	Row1 is Row+1,
	forall(between(0, Row1, I),
	       (   new(T, tube(I, D)),
		   send(Ch, append, T),
		   send(D, display, T))).

PureBasic

Translation of: Unicon
Sample display of PureBasic solution
Global pegRadius, pegSize, pegSize2, height, width, delay, histogramSize, ball

Procedure eventLoop()
  Protected event
  Repeat
    event = WindowEvent()
    If event = #PB_Event_CloseWindow
      End
    EndIf
  Until event = 0 
EndProcedure
 
Procedure animate_actual(x1, y1, x2, y2, steps)
  Protected x.f, y.f, xstep.f, ystep.f, i, lastX.f, lastY.f
  x = x1
  y = y1
  xstep = (x2 - x1)/steps
  ystep = (y2 - y1)/steps
  For i = 1 To steps
    lastX = x
    lastY = y
    StartDrawing(CanvasOutput(0))
      DrawingMode(#PB_2DDrawing_XOr)
      Circle(x, y, pegRadius, RGB(0, 255, 255))
    StopDrawing()
    eventLoop()
    Delay(delay)      ; wait in ms
    StartDrawing(CanvasOutput(0))
      DrawingMode(#PB_2DDrawing_XOr)
      Circle(x, y, pegRadius, RGB(0, 255, 255))
    StopDrawing()
    eventLoop()
    x + xstep
    y + ystep
  Next
EndProcedure

Procedure draw_ball(xpos, ypos)                      
  Static Dim ballcounts(0) ;tally drop positions
  If xpos > ArraySize(ballcounts())
    Redim ballcounts(xpos)
  EndIf 
  ballcounts(xpos) + 1
  animate_actual(xpos, ypos, xpos, height - ballcounts(xpos) * pegSize, 20)
  StartDrawing(CanvasOutput(0))
    Circle(xpos, height - ballcounts(xpos) * pegSize, pegRadius, RGB(255, 0, 0))
  StopDrawing()
  eventLoop()
  If ballcounts(xpos) <= histogramSize
    ProcedureReturn 1 
  EndIf
  SetWindowTitle(0, "Ended after " + Str(ball) + " balls") ;histogramSize exceeded
EndProcedure 

Procedure animate(x1, y1, x2, y2)
  animate_actual(x1, y1, x2, y1, 4)
  animate_actual(x2, y1, x2, y2, 10)
EndProcedure 

Procedure galton(pegRows)
  ;drop a ball into the galton box
  Protected xpos, ypos, i, oldX, oldY

  oldX = width / 2 - pegSize / 2
  xpos = oldX
  oldY = pegSize
  ypos = oldY
  animate_actual(oldX, 0, xpos, ypos, 10)
  For i = 1 To pegRows 
    If Random(1)
      xpos + pegSize 
    Else
      xpos - pegSize
    EndIf 
    ypos + pegSize2
    animate(oldX, oldY, xpos, ypos)
    oldX = xpos
    oldY = ypos
  Next
  
  ProcedureReturn draw_ball(xpos, ypos)
EndProcedure

Procedure setup_window(numRows, ballCount)  
  ;Draw numRows levels of pegs
  Protected xpos, ypos, i, j
  
  width = (2 * numRows + 2) * pegSize
  histogramSize = (ballCount + 2) / 3
  If histogramSize > 500 / pegSize: histogramSize = 500 / pegSize: EndIf
  height = width + histogramSize * pegSize
  OpenWindow(0, 0, 0, width, height, "Galton box animation", #PB_Window_SystemMenu)
  CanvasGadget(0, 0, 0, width, height) 
  
  StartDrawing(CanvasOutput(0))
    Box(0, 0, width, height, RGB($EB, $EB, $EB))
    For i = 1 To numRows
      ypos = i * pegSize2
      xpos = width / 2 - (i - 1) * pegSize - pegSize / 2
      For j = 1 To i
        Circle(xpos, ypos, pegRadius, RGB(0, 0, 255))
        xpos + pegSize2
      Next
    Next
    For i = 1 To numRows
      Line((numRows - i + 1) * pegSize2 - pegSize / 2, width - pegSize, 1, histogramSize * pegSize, 0)
    Next 
  StopDrawing()
EndProcedure

;based on the galton box simulation from Unicon book
Define pegRows = 10, ballCount
pegRadius = 4
pegSize = pegRadius * 2 + 1
pegSize2 = pegSize * 2 
delay = 2                      ; ms delay

Repeat
  ballCount = Val(InputRequester("Galton box simulator","How many balls to drop?", "100"))
Until ballCount > 0

setup_window(pegRows, ballCount)
eventLoop()
For ball = 1 To ballCount
  If Not galton(pegRows): Break: EndIf
Next
Repeat: eventLoop(): ForEver

Python

#!/usr/bin/python

import sys, os
import random
import time

def print_there(x, y, text):
     sys.stdout.write("\x1b7\x1b[%d;%df%s\x1b8" % (x, y, text))
     sys.stdout.flush()


class Ball():
    def __init__(self):
        self.x = 0
        self.y = 0
        
    def update(self):
        self.x += random.randint(0,1)
        self.y += 1

    def fall(self):
        self.y +=1


class Board():
    def __init__(self, width, well_depth, N):
        self.balls = []
        self.fallen = [0] * (width + 1)
        self.width = width
        self.well_depth = well_depth
        self.N = N
        self.shift = 4
        
    def update(self):
        for ball in self.balls:
            if ball.y < self.width:
                ball.update()
            elif ball.y < self.width + self.well_depth - self.fallen[ball.x]:
                ball.fall()
            elif ball.y == self.width + self.well_depth - self.fallen[ball.x]:
                self.fallen[ball.x] += 1
            else:
                pass
                
    def balls_on_board(self):
        return len(self.balls) - sum(self.fallen)
                
    def add_ball(self):
        if(len(self.balls) <= self.N):
            self.balls.append(Ball())

    def print_board(self):
        for y in range(self.width + 1):
            for x in range(y):
                print_there( y + 1 ,self.width - y + 2*x + self.shift + 1, "#")
    def print_ball(self, ball):
        if ball.y <= self.width:
            x = self.width - ball.y + 2*ball.x + self.shift
        else:
            x = 2*ball.x + self.shift
        y = ball.y + 1
        print_there(y, x, "*")
         
    def print_all(self):
        print(chr(27) + "[2J")
        self.print_board();
        for ball in self.balls:
            self.print_ball(ball)


def main():
    board = Board(width = 15, well_depth = 5, N = 10)
    board.add_ball() #initialization
    while(board.balls_on_board() > 0):
         board.print_all()
         time.sleep(0.25)
         board.update()
         board.print_all()
         time.sleep(0.25)
         board.update()
         board.add_ball()


if __name__=="__main__":
    main()

Racket

This does not use the default #lang racket. Required is advanced student with teachpacks universe and image. Multiple balls are added each step, but they do not collide.

;a ball's position...row is a natural number and col is an integer where 0 is the center
(define-struct pos (row col))
;state of simulation...list of all positions and vector of balls (index = bin)
(define-struct st (poss bins))
;increment vector @i
(define (vector-inc! v i) (vector-set! v i (add1 (vector-ref v i))))

(define BALL-RADIUS 6)
;for balls to fit perfectly between diamond-shaped pins, the side length is
;determined by inscribing the diamond in the circle
(define PIN-SIDE-LENGTH (* (sqrt 2) BALL-RADIUS))
;ultimate pin width and height
(define PIN-WH (* 2 BALL-RADIUS))
(define PIN-HOR-SPACING (* 2 PIN-WH))
;vertical space is the height of an equilateral triangle with side length = PIN-HOR-SPACING
(define PIN-VER-SPACING (* 1/2 (sqrt 3) PIN-HOR-SPACING))
;somewhat copying BASIC256's graphics
;determines how thick the outline will be
(define FILL-RATIO 0.7)
;freeze is a function that converts the drawing code into an actual bitmap forever
(define PIN (freeze (overlay (rotate 45 (square (* FILL-RATIO PIN-SIDE-LENGTH) "solid" "purple"))
                             (rotate 45 (square PIN-SIDE-LENGTH "solid" "magenta")))))
(define BALL (freeze (overlay (circle (* FILL-RATIO BALL-RADIUS) "solid" "green")
                              (circle BALL-RADIUS "solid" "dark green"))))
(define BIN-COLOR (make-color 255 128 192))
;# balls bin can fit
(define BIN-CAPACITY 10)
(define BIN-HEIGHT (* BIN-CAPACITY PIN-WH))
(define BIN (freeze (beside/align "bottom"
                                  (line 0 BIN-HEIGHT BIN-COLOR)
                                  (line PIN-WH 0 BIN-COLOR)
                                  (line 0 BIN-HEIGHT BIN-COLOR))))

(define draw-background
  (let ([background #f])
    (λ (height)
      (if (image? background)
          background
          (let* ([w (+ (image-width BIN) (* PIN-HOR-SPACING height))]
                 [h (+ PIN-WH (image-height BIN) (* PIN-VER-SPACING height))]
                 [draw-background (λ () (rectangle w h "solid" "black"))])
            (begin (set! background (freeze (draw-background))) background))))))

;draws images using x horizontal space between center points
(define (spaced/x x is)
  (if (null? is)
      (empty-scene 0 0)
      (let spaced/x ([n 1] [i (car is)] [is (cdr is)])
        (if (null? is)
            i
            (overlay/xy i (* -1 n x) 0 (spaced/x (add1 n) (car is) (cdr is)))))))
  
(define (draw-pin-row r) (spaced/x PIN-HOR-SPACING (make-list (add1 r) PIN)))

;draws all pins, using saved bitmap for efficiency
(define draw-pins
  (let ([bmp #f])
    (λ (height)
      (let ([draw-pins (λ () (foldl (λ (r i) (overlay/align/offset
                                              ;vertically line up all pin rows
                                              "center" "bottom" (draw-pin-row r)
                                              ;shift down from the bottom of accum'ed image by ver spacing
                                              0 (- PIN-VER-SPACING) i))
                                    (draw-pin-row 0) (range 1 height 1)))])
        (if (image? bmp)
            bmp
            (begin (set! bmp (freeze (draw-pins))) bmp))))))

(define (draw-ball p i)
  ;the ball starts at the top of the image
  (overlay/align/offset "center" "top" BALL (* -1 (pos-col p) PIN-WH) (* -1 (pos-row p) PIN-VER-SPACING) i))

;bin has balls added from bottom, stacked exactly on top of each other
;the conditional logic is needed because above can't handle 0 or 1 things
(define (draw-bin n)
  (if (zero? n)
      BIN
      (overlay/align "center" "bottom"
                     (if (= n 1) BALL (apply above (make-list n BALL)))
                     BIN)))

;main drawing function
(define (draw height s)
  (let* ([bins (spaced/x PIN-HOR-SPACING (map draw-bin (vector->list (st-bins s))))]
         ;pins above bins
         [w/pins (above (draw-pins height) bins)]
         ;draw this all one ball diameter (PIN-WH) below top
         [w/background (overlay/align/offset "center" "top" w/pins
                                             0 (- PIN-WH) (draw-background height))])
    ;now accumulate in each ball
    (foldl draw-ball w/background (st-poss s))))

;a ball moves down by increasing its row and randomly changing its col by -1 or 1
(define (next-row height p)
  (make-pos (add1 (pos-row p))
            (+ -1 (* 2 (random 2)) (pos-col p))))

;each step, every ball goes to the next row and new balls are added at the top center
;balls that fall off go into bins
(define (tock height s)
  (let* ([new-ps (map (λ (p) (next-row height p)) (st-poss s))]
         ;live balls haven't gone past the last row of pins
         [live (filter (λ (p) (< (pos-row p) height)) new-ps)]
         ;dead balls have (partition from normal Racket would be useful here...)
         [dead (filter (λ (p) (>= (pos-row p) height)) new-ps)]
         ;adjust col from [-x,x] to [0,2x]
         [bin-indices (map (λ (p) (quotient (+ (pos-col p) height) 2)) dead)])
    ;add new balls to the live balls
    (make-st (append (make-list (random 4) (make-pos 0 0)) live)
             ;sum dead ball positions into bins
             (begin (for-each (λ (i) (vector-inc! (st-bins s) i)) bin-indices)
                    (st-bins s)))))

;run simulation with empty list of positions to start, stepping with "tock" and drawing with "draw"
(define (run height)
  (big-bang (make-st '() (make-vector (add1 height) 0))
            (on-tick (λ (ps) (tock height ps)) 0.5)
            (to-draw (λ (ps) (draw height ps)))))

Raku

(formerly Perl 6)

UPPER HALF BLOCK and LOWER HALF BLOCK alternate to give a somewhat smooth animation.
Works with: rakudo version 2015-09-12
my $row-count = 6;
 
constant $peg = "*";
constant @coin-icons = "\c[UPPER HALF BLOCK]", "\c[LOWER HALF BLOCK]";
 
sub display-board(@positions, @stats is copy, $halfstep) {
    my $coin = @coin-icons[$halfstep.Int];
 
    state @board-tmpl = {
        # precompute a board
        my @tmpl;
        sub out(*@stuff) {
            @tmpl.push: $[@stuff.join>>.ords.flat];
        }
        # three lines of space above
        for 1..3 {
            out "  ", " " x (2 * $row-count);
        }
        # $row-count lines of pegs
        for flat ($row-count...1) Z (1...$row-count) -> $spaces, $pegs {
            out "  ", " " x $spaces, ($peg xx $pegs).join(" "), " " x $spaces;
        }
        # four lines of space below
        for 1..4 {
            out "  ", " " x (2 * $row-count);
        }
        @tmpl
    }();
 
    my $midpos = $row-count + 2;
 
    my @output;
    {
        # collect all the output and output it all at once at the end
        sub say(Str $foo) {
            @output.push: $foo, "\n";
        }
        sub print(Str $foo) {
            @output.push: $foo;
        }
 
        # make some space above the picture
        say "" for ^10;
 
        my @output-lines = map { [ @$_ ] }, @board-tmpl;
        # place the coins
        for @positions.kv -> $line, $pos {
            next unless $pos.defined;
            @output-lines[$line][$pos + $midpos] = $coin.ord;
        }
        # output the board with its coins
        for @output-lines -> @line {
            say @line.chrs;
        }
 
        # show the statistics
        my $padding = 0;
        while any(@stats) > 0 {
            $padding++;
            print "  ";
            @stats = do for @stats -> $stat {
                given $stat {
                    when 1 {
                        print "\c[UPPER HALF BLOCK]";
                        $stat - 1;
                    }
                    when * <= 0 {
                        print " ";
                        0
                    }
                    default {
                        print "\c[FULL BLOCK]";
                        $stat - 2;
                    }
                }
            }
            say "";
        }
        say "" for $padding...^10;
    }
    say @output.join("");
}
 
sub simulate($coins is copy) {
    my $alive = True;
 
    sub hits-peg($x, $y) {
        if 3 <= $y < 3 + $row-count and -($y - 2) <= $x <= $y - 2 {
            return not ($x - $y) %% 2;
        }
        return False;
    }
 
    my @coins = Int xx (3 + $row-count + 4);
    my @stats = 0 xx ($row-count * 2);
    # this line will dispense coins until turned off.
    @coins[0] = 0;
    while $alive {
        $alive = False;
        # if a coin falls through the bottom, count it
        given @coins[*-1] {
            when *.defined {
                @stats[$_ + $row-count]++;
            }
        }
 
        # move every coin down one row
        for ( 3 + $row-count + 3 )...1 -> $line {
            my $coinpos = @coins[$line - 1];
 
            @coins[$line] = do if not $coinpos.defined {
                Nil
            } elsif hits-peg($coinpos, $line) {
                # when a coin from above hits a peg, it will bounce to either side.
                $alive = True;
                ($coinpos - 1, $coinpos + 1).pick;
            } else {
                # if there was a coin above, it will fall to this position.
                $alive = True;
                $coinpos;
            }
        }
        # let the coin dispenser blink and turn it off if we run out of coins
        if @coins[0].defined {
            @coins[0] = Nil
        } elsif --$coins > 0 {
            @coins[0] = 0 
        }
 
        # smooth out the two halfsteps of the animation
        my $start-time;
        ENTER { $start-time = now }
        my $wait-time = now - $start-time;
 
        sleep 0.1 - $wait-time if $wait-time < 0.1;
        for @coin-icons.keys {
            sleep $wait-time max 0.1;
            display-board(@coins, @stats, $_);
        }
    }
}
 
sub MAIN($coins = 20, $peg-lines = 6) {
    $row-count = $peg-lines;
    simulate($coins);
}

REXX

The REXX version displays an ASCII version of a working Galton box.

Balls are dropped continuously   (up to a number specified or the default),   the default is enough rows of
pins to fill the top   1/3   rows of the terminal screen.

/*REXX pgm simulates Sir Francis Galton's box, aka: Galton Board, quincunx, bean machine*/
trace off                                        /*suppress any messages for negative RC*/
if !all(arg())  then exit                        /*Any documentation was wanted?   Done.*/
signal on halt                                   /*allow the user to  halt  the program.*/
parse arg rows balls freeze seed .               /*obtain optional arguments from the CL*/
if rows ==''  |  rows==","   then   rows=   0    /*Not specified?  Then use the default.*/
if balls==''  | balls==","   then  balls= 100    /* "      "         "   "   "     "    */
if freeze=='' | freeze==","  then freeze=   0    /* "      "         "   "   "     "    */
if datatype(seed, 'W')   then call random ,,seed /*Was a seed specified?  Then use seed.*/
pin  = '·';              ball = '☼'              /*define chars for a  pin  and a  ball.*/
parse value  scrsize()   with   sd  sw  .        /*obtain the terminal depth and width. */
if sd==0  then sd= 40                            /*Not defined by the OS?  Use a default*/
if sw==0  then sw= 80                            /* "     "     "  "   "    "  "    "   */
sd= sd - 3                                       /*define the usable       screen depth.*/
sw= sw - 1;  if sw//2  then sw= sw - 1           /*   "    "    "     odd     "   width.*/
if rows==0  then rows= (sw - 2 ) % 3             /*pins are on the first third of screen*/
call gen                                         /*gen a triangle of pins with some rows*/
           do step=1;  call drop;  call show     /*show animation 'til run out of balls.*/
           end   /*step*/                        /* [↑]  the dropping/showing   "   "   */
exit 0                                           /*stick a fork in it,  we're all done. */
/*──────────────────────────────────────────────────────────────────────────────────────*/
gen: @.=;  do r=0  by 2  to rows;     $=         /*build a triangle of pins for the box.*/
                do pins=1  for r%2;   $= $  pin  /*build a row of pins to be displayed. */
                end   /*pins*/
           @.r= center( strip($, 'T'), sw)       /*an easy method to build a triangle.  */
           end     /*r*/;     #= 0;       return /*#:   is the number of balls dropped. */
/*──────────────────────────────────────────────────────────────────────────────────────*/
drop: static= 1                                  /*used to indicate all balls are static*/
        do c=sd-1  by -1  for sd-1;    n= c + 1  /*D:  current row;   N:  the next row. */
        x= pos(ball, @.c);             y= x - 1  /*X:  position of a ball on the C line.*/
        if x==0  then iterate                    /*No balls here?  Then nothing to drop.*/
          do forever;   y= pos(ball, @.c, y+1)   /*drop most balls down to the next row.*/
          if y==0  then iterate c                /*down with this row, go look at next. */
          z= substr(@.n, y, 1)                   /*another ball is blocking this fall.  */
          if z==' '  then do;  @.n= overlay(ball, @.n, y)   /*drop a ball straight down.*/
                               @.c= overlay(' ' , @.c, y)   /*make current ball a ghost.*/
                               static= 0                    /*indicate balls are moving.*/
                               iterate                      /*go keep looking for balls.*/
                          end
          if z==pin  then do;  ?= random(,999);   d= -1     /*assume falling to the left*/
                                    if ?//2  then d=  1     /*if odd random#, fall right*/
                               if substr(@.n, y+d, 1)\==' '  then iterate /*blocked fall*/
                               @.n= overlay(ball, @.n, y+d)
                               @.c= overlay(' ' , @.c, y  )
                               static= 0                    /*indicate balls are moving.*/
                               iterate                      /*go keep looking for balls.*/
                          end
          end   /*forever*/
        end     /*c*/                  /* [↓]   step//2    is used to avoid collisions. */
                                                            /* [↓]  drop a new ball ?   */
     if #<balls & step//2  then do;    @.1= center(ball, sw+1);      # = # + 1;        end
                           else if static  then exit 2      /*insure balls are static.  */
     return
/*──────────────────────────────────────────────────────────────────────────────────────*/
show: !cls;    do LR=sd  by -1  until @.LR\==''                 /*LR:  last row of data.*/
               end   /*LR*/;                 ss= 'step' step    /* [↓]   display a row. */
        do r=1  for LR; _= strip(@.r, 'T');  if r==2  then _= overlay(ss, _, sw-12); say _
        end   /*r*/;   if step==freeze  then do;   say 'press ENTER ···';    pull;   end
      return
/*══════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════*/
halt:  say '***warning***  REXX program'   !fn    "execution halted by user.";      exit 1
!all:  !!=!;!=space(!);upper !;call !fid;!nt=right(!var('OS'),2)=='NT';!cls=word('CLS VMFCLEAR CLRSCREEN',1+!cms+!tso*2);if arg(1)\==1 then return 0;if wordpos(!,'? ?SAMPLES ?AUTHOR ?FLOW')==0 then return 0;!call=']$H';call '$H' !fn !;!call=;return 1
!cal:  if symbol('!CALL')\=="VAR"  then !call=;                                  return !call
!env:  !env='ENVIRONMENT';    if !sys=='MSDOS' | !brexx | !r4 | !roo  then !env= 'SYSTEM';    if !os2  then !env= 'OS2'!env;    !ebcdic=  2=='f2'x;    if !crx  then !env= 'DOS';                     return
!fid:  parse upper source !sys !fun !fid . 1 . . !fn !ft !fm .;  call !sys;   if !dos  then do;  _= lastpos('\', !fn);  !fm= left(!fn, _);  !fn= substr(!fn, _+1);  parse var !fn !fn '.' !ft;  end;  return word(0 !fn !ft !fm, 1 + ('0'arg(1) ) )
!rex:  parse upper version !ver !vernum !verdate .; !brexx= 'BY'==!vernum; !kexx= 'KEXX'==!ver; !pcrexx= 'REXX/PERSONAL'==!ver | 'REXX/PC'==!ver; !r4= 'REXX-R4'==!ver; !regina= 'REXX-REGINA'==left(!ver, 11); !roo= 'REXX-ROO'==!ver; call !env;  return
!sys:  !cms= !sys=='CMS';  !os2= !sys=='OS2';  !tso= !sys=='TSO' | !sys=='MVS';  !vse= !sys=='VSE';  !dos= pos('DOS', !sys)\==0 | pos('WIN', !sys)\==0 | !sys=='CMD';  !crx= left(!sys, 6)=='DOSCRX';  call !rex;  return
!var:  call !fid;  if !kexx  then return space( dosenv( arg(1) ) );              return space( value( arg(1), , !env) )

Programming note:   the last seven lines of this REXX program are some general purpose (boilerplate code) that, among other things, finds:

  •   the REXX program's filename, filetype (file extension), and filemode (and/or path)
  •   if the user wants documentation presented (not germane to this REXX program)
  •   the environment name (not germane)
  •   what command to be used to clear the terminal screen
  •   the name of the operating system being used (not germane)
  •   the name of the REXX interpreter being used (not germane)
  •   various other bits of information (not germane)

It is only intended to be used to make this particular REXX program independent of any particular REXX interpreter and/or independent of knowing which program is to be used for clearing the terminal screen.   As such, the boilerplate code isn't commented and isn't intended to be a teaching tool.

This REXX program makes use of   SCRSIZE   REXX program (or BIF) which is used to determine the screen
width and depth of the terminal (console).   Some REXXes don't have this BIF.

The   SCRSIZE.REX   REXX program is included here   ───►   SCRSIZE.REX.

The terminal size used for this display was   64x96.

output   when the REXX program was "stopped" by using the inputs of     , , 100   so as to freeze the program to capture a screenshot:

(Shown at   2/3   size.)

                                              ☼·                          step 100

                                              ·☼·

                                             · ·☼·

                                            · ·☼· ·

                                          ☼· · · · ·

                                          · · ·☼· · ·

                                         ·☼· · · · · ·

                                        · · · · ·☼· · ·

                                       · · · · · · ·☼· ·

                                      · · ·☼· · · · · · ·

                                     · · · ·☼· · · · · · ·

                                    · · · ·☼· · · · · · · ·

                                   · · ·☼· · · · · · · · · ·

                                  · · · · · · · ·☼· · · · · ·

                                 · · · · · · ·☼· · · · · · · ·

                                                ☼

                                                  ☼

                                                ☼

                                            ☼

                                                  ☼

                                                      ☼

                                                        ☼

                                                ☼

                                      ☼

                                              ☼

                                                    ☼

                                            ☼

                                            ☼

                                            ☼ ☼   ☼
                                            ☼ ☼   ☼
                                      ☼   ☼ ☼ ☼ ☼ ☼     ☼
                                      ☼ ☼ ☼ ☼ ☼ ☼ ☼ ☼   ☼
press ENTER ···
output   when using the above inputs;   this is the final screenshot:

(Shown at   2/3   size.)

                                               ·                          step 350

                                              · ·

                                             · · ·

                                            · · · ·

                                           · · · · ·

                                          · · · · · ·

                                         · · · · · · ·

                                        · · · · · · · ·

                                       · · · · · · · · ·

                                      · · · · · · · · · ·

                                     · · · · · · · · · · ·

                                    · · · · · · · · · · · ·

                                   · · · · · · · · · · · · ·

                                  · · · · · · · · · · · · · ·

                                 · · · · · · · · · · · · · · ·

                                            ☼
                                            ☼ ☼
                                            ☼ ☼
                                            ☼ ☼ ☼
                                            ☼ ☼ ☼
                                            ☼ ☼ ☼
                                            ☼ ☼ ☼
                                            ☼ ☼ ☼ ☼
                                            ☼ ☼ ☼ ☼
                                            ☼ ☼ ☼ ☼
                                            ☼ ☼ ☼ ☼
                                            ☼ ☼ ☼ ☼
                                            ☼ ☼ ☼ ☼
                                            ☼ ☼ ☼ ☼
                                            ☼ ☼ ☼ ☼
                                            ☼ ☼ ☼ ☼
                                            ☼ ☼ ☼ ☼
                                            ☼ ☼ ☼ ☼
                                            ☼ ☼ ☼ ☼
                                            ☼ ☼ ☼ ☼
                                          ☼ ☼ ☼ ☼ ☼ ☼
                                          ☼ ☼ ☼ ☼ ☼ ☼
                                          ☼ ☼ ☼ ☼ ☼ ☼
                                        ☼ ☼ ☼ ☼ ☼ ☼ ☼ ☼
                                        ☼ ☼ ☼ ☼ ☼ ☼ ☼ ☼
                                        ☼ ☼ ☼ ☼ ☼ ☼ ☼ ☼
                                      ☼ ☼ ☼ ☼ ☼ ☼ ☼ ☼ ☼
                                      ☼ ☼ ☼ ☼ ☼ ☼ ☼ ☼ ☼ ☼
                                      ☼ ☼ ☼ ☼ ☼ ☼ ☼ ☼ ☼ ☼
                                      ☼ ☼ ☼ ☼ ☼ ☼ ☼ ☼ ☼ ☼ 

Ruby

Library: Shoes
Sample display of Ruby solution
$rows_of_pins = 12
$width = $rows_of_pins * 10 + ($rows_of_pins+1)*14

Shoes.app(
    :width => $width + 14,
    :title => "Galton Box"
) do
  @bins = Array.new($rows_of_pins+1, 0)

  @x_coords = Array.new($rows_of_pins) {Array.new}
  @y_coords = Array.new($rows_of_pins)
  stack(:width => $width) do
    stroke gray
    fill gray
    1.upto($rows_of_pins) do |row|
      y = 14 + 24*row
      @y_coords[row-1] = y
      row.times do |i|
        x = $width / 2 + (i - 0.5*row)*24 + 14
        @x_coords[row-1] << x
        oval x+2, y, 6
      end
    end
  end
  @y_coords << @y_coords[-1] + 24
  @x_coords << @x_coords[-1].map {|x| x-12} + [@x_coords[-1][-1]+12]

  @balls = stack(:width => $width) do
    stroke red
    fill red
  end.move(0,0)

  @histogram = stack(:width => $width) do
    nostroke
    fill black
  end.move(0, @y_coords[-1] + 10)

  @paused = false
  keypress do |key|
    case key
    when "\x11", :control_q
      exit
    when "\x10", :control_p
      @paused = !@paused
    end
  end

  @ball_row = 0
  @ball_col = 0
  animate(2*$rows_of_pins) do
    if not @paused
      y = @y_coords[@ball_row] - 12
      x = @x_coords[@ball_row][@ball_col]
      @balls.clear {oval x, y, 10}
      @ball_row += 1
      if @ball_row <= $rows_of_pins
        @ball_col += 1 if rand >= 0.5
      else
        @bins[@ball_col] += 1
        @ball_row = @ball_col = 0
        update_histogram
      end
    end
  end

  def update_histogram
    y = @y_coords[-1] + 10
    @histogram.clear do
      @bins.each_with_index do |num, i|
        if num > 0
          x = @x_coords[-1][i]
          rect x-6, 0, 24, num
        end
      end
    end
  end
end

Tcl

Translation of: C
package require Tcl 8.6

oo::class create GaltonBox {
    variable b w h n x y cnt step dropping

    constructor {BALLS {NUMPEGS 5} {HEIGHT 24}} {
	set n $NUMPEGS
	set w [expr {$n*2 + 1}]
	set h $HEIGHT
	puts -nonewline "\033\[H\033\[J"
	set x [set y [lrepeat $BALLS 0]]
	set cnt 0
	set step 0
	set dropping 1

	set b [lrepeat $h [lrepeat $w " "]]
	for {set i 0} {$i < $n} {incr i} {
	    for {set j [expr {-$i}]} {$j <= $i} {incr j 2} {
		lset b [expr {2*$i+2}] [expr {$j+$w/2}] "*"
	    }
	}
    }

    method show {} {
	puts -nonewline "\033\[H"
	set oldrow {}
	foreach row $b {
	    foreach char $row oldchar $oldrow {
		if {$char ne "*"} {
		    puts -nonewline "$char "
		} elseif {$oldchar eq " "} {
		    puts -nonewline "\033\[32m*\033\[m "
		} else {
		    puts -nonewline "\033\[31m*\033\[m "
		}
	    }
	    set oldrow $row
	    puts ""
	}
    }

    method Move idx {
	set xx [lindex $x $idx]
	set yy [lindex $y $idx]
	set kill 0

	if {$yy < 0} {return 0}
	if {$yy == $h-1} {
	    lset y $idx -1
	    return 0
	}

	switch [lindex $b [incr yy] $xx] {
	    "*" {
		incr xx [expr {2*int(2 * rand()) - 1}]
		if {[lindex $b [incr yy -1] $xx] ne " "} {
		    set dropping 0
		}
	    }
	    "o" {
		incr yy -1
		set kill 1
	    }
	}

	set c [lindex $b [lindex $y $idx] [lindex $x $idx]]
	lset b [lindex $y $idx] [lindex $x $idx] " "
	lset b $yy $xx $c
	if {$kill} {
	    lset y $idx -1
	} else {
	    lset y $idx $yy
	}
	lset x $idx $xx
	return [expr {!$kill}]
    }

    method step {} {
	set moving 0
	for {set i 0} {$i < $cnt} {incr i} {
	    set moving [expr {[my Move $i] || $moving}]
	}
	if {2 == [incr step] && $cnt < [llength $x] && $dropping} {
	    set step 0
	    lset x $cnt [expr {$w / 2}]
	    lset y $cnt 0
	    if {[lindex $b [lindex $y $cnt] [lindex $x $cnt]] ne " "} {
		return 0
	    }
	    lset b [lindex $y $cnt] [lindex $x $cnt] "o"
	    incr cnt
	}
	return [expr {($moving || $dropping)}]
    }
}

GaltonBox create board 1024 {*}$argv
while true {
    board show
    if {[board step]} {after 60} break
}

After a sample run with input parameters 10 55:

                                          
                                          
                    *                     
                                          
                  *   *                   
                                          
                *   *   *                 
                                          
              *   *   *   *               
                                          
            *   *   *   *   *             
                                          
          *   *   *   *   *   *           
                                          
        *   *   *   *   *   *   *         
                                          
      *   *   *   *   *   *   *   *       
                                          
    *   *   *   *   *   *   *   *   *     
                    o   o                 
  *   *   *   *   * o * o *   *   *   *   
                    o   o                 
                    o   o                 
                    o   o                 
                    o   o                 
                    o   o                 
                    o   o                 
                    o   o                 
                    o   o                 
                    o   o                 
            o       o   o                 
            o       o   o                 
            o       o   o                 
            o       o   o                 
            o       o   o                 
            o   o   o   o   o             
            o   o   o   o   o             
            o   o   o   o   o             
            o   o   o   o   o             
            o   o   o   o   o             
            o   o   o   o   o             
            o   o   o   o   o             
            o   o   o   o   o             
            o   o   o   o   o             
            o   o   o   o   o             
            o   o   o   o   o             
            o   o   o   o   o             
            o   o   o   o   o             
        o   o   o   o   o   o             
        o   o   o   o   o   o   o         
    o   o   o   o   o   o   o   o         
    o   o   o   o   o   o   o   o         
    o   o   o   o   o   o   o   o         
    o   o   o   o   o   o   o   o         
    o   o   o   o   o   o   o   o         

There is a much more comprehensive solution to this on the Tcler's Wiki.

Wren

Translation of: D
Library: Wren-iterate
import "random" for Random
import "./iterate" for Reversed

var boxW = 41       // Galton box width.
var boxH = 37       // Galton box height.
var pinsBaseW = 19  // Pins triangle base.
var nMaxBalls = 55  // Number of balls.

var centerH = pinsBaseW + (boxW - pinsBaseW * 2 + 1) / 2 - 1
var Rand = Random.new()

class Cell {
    static EMPTY  { " " }
    static BALL   { "o" }
    static WALL   { "|" }
    static CORNER { "+" }
    static FLOOR  { "-" }
    static PIN    { "." }
}

/* Galton box. Will be printed upside down. */
var Box = List.filled(boxH, null)
for (i in 0...boxH) Box[i] = List.filled(boxW, Cell.EMPTY)

class Ball {
    construct new(x, y) {
        if (Box[x][y] != Cell.EMPTY) Fiber.abort("The cell at (x, y) is not empty.")
        Box[y][x] = Cell.BALL
        _x = x
        _y = y
    }

    doStep() {
        if (_y <= 0) return  // Reached the bottom of the box.
        var cell = Box[_y - 1][_x]
        if (cell == Cell.EMPTY) {
            Box[_y][_x] = Cell.EMPTY
            _y = _y - 1
            Box[_y][_x] = Cell.BALL
        } else if (cell == Cell.PIN) {
            Box[_y][_x] = Cell.EMPTY
            _y = _y - 1
            if (Box[_y][_x - 1] == Cell.EMPTY && Box[_y][_x + 1] == Cell.EMPTY) {
                _x = _x + Rand.int(2) * 2 - 1
                Box[_y][_x] = Cell.BALL
                return
            } else if (Box[_y][_x - 1] == Cell.EMPTY){
                _x = _x + 1
            } else _x = _x - 1
            Box[_y][_x] = Cell.BALL
        } else {
            // It's frozen - it always piles on other balls.
        }
    }
}

var initializeBox = Fn.new {
    // Set ceiling and floor:
    Box[0][0] = Cell.CORNER
    Box[0][boxW - 1] = Cell.CORNER
    for (i in 1...boxW - 1) Box[0][i] = Cell.FLOOR
    for (i in 0...boxW) Box[boxH - 1][i] = Box[0][i]

    // Set walls:
    for (r in 1...boxH - 1) {
        Box[r][0] = Cell.WALL
        Box[r][boxW - 1] = Cell.WALL
    }
 
    // Set pins:
    for (nPins in 1..pinsBaseW) {
        for (pin in 0...nPins) {
            Box[boxH - 2 - nPins][centerH + 1 - nPins + pin * 2] = Cell.PIN
        }
    }
}

var drawBox = Fn.new() {
    for (row in Reversed.new(Box, 1)) {
        for (c in row) System.write(c)
        System.print()
    }
}

initializeBox.call()
var balls = []
for (i in 0...nMaxBalls + boxH) {
    System.print("\nStep %(i):")
    if (i < nMaxBalls) balls.add(Ball.new(centerH, boxH - 2))  // Add ball.
    drawBox.call()

    // Next step for the simulation.
    // Frozen balls are kept in balls list for simplicity
    for (b in balls) b.doStep()
}
Output:

Sample output, showing the last step only:

Step 91:
+---------------------------------------+
|                                       |
|                   .                   |
|                  . .                  |
|                 . . .                 |
|                . . . .                |
|               . . . . .               |
|              . . . . . .              |
|             . . . . . . .             |
|            . . . . . . . .            |
|           . . . . . . . . .           |
|          . . . . . . . . . .          |
|         . . . . . . . . . . .         |
|        . . . . . . . . . . . .        |
|       . . . . . . . . . . . . .       |
|      . . . . . . . . . . . . . .      |
|     . . . . . . . . . . . . . . .     |
|    . . . . . . . . . . . . . . . .    |
|   . . . . . . . . . . . . . . . . .   |
|  . . . . . . . . . . . . . . . . . .  |
| . . . . . . . . . . . . . . . . . . . |
|                                       |
|                                       |
|                o                      |
|                o                      |
|                o                      |
|                o o                    |
|                o o                    |
|                o o                    |
|                o o o   o              |
|                o o o   o              |
|                o o o   o              |
|            o   o o o   o              |
|            o o o o o o o     o        |
|            o o o o o o o   o o        |
|          o o o o o o o o o o o o      |
+---------------------------------------+

XPL0

This Peeks into some IBM-PC specific locations and hence is not entirely portable.

include c:\cxpl\codes;          \intrinsic code declarations
define  Balls = 80;             \maximum number of balls
int     Bx(Balls), By(Balls),   \character cell coordinates of each ball
        W, I, J, Peg, Dir;
[W:= Peek($40, $4A);            \get screen width in characters
Clear;  CrLf(6);  CrLf(6);
for Peg:= 1 to 10 do                            \draw pegs
        [for I:= 1 to 12-Peg do ChOut(6,^ );    \space over to first peg
         for I:= 1 to Peg do [ChOut(6,^.);  ChOut(6,^ )];
        CrLf(6);
        ];
for J:= 0 to 12-1 do                            \draw slots
        [for I:= 0 to 12-1 do [ChOut(6,^:);  ChOut(6,^ )];
        CrLf(6);
        ];
for I:= 0 to 23-1 do ChOut(6,^.);               \draw bottom
for I:= 0 to Balls-1 do                         \make source of balls at top
        [Bx(I):= 11;  By(I):= 1];
Attrib($C);                                     \make balls bright red
repeat                                          \balls away! ...
    for I:= 0 to Balls-1 do                     \for all the balls ...
        [Cursor(Bx(I), By(I));  ChOut(6, ^ );   \erase ball's initial position
        if Peek($B800, (Bx(I)+(By(I)+1)*W)*2) = ^ \is ball above empty location?
        then    By(I):= By(I)+1                 \yes: fall straight down
        else    [Dir:= Ran(3)-1;                \no: randomly fall right or left
                if Peek($B800, (Bx(I)+Dir+(By(I)+1)*W)*2) = ^  then
                        [Bx(I):= Bx(I)+Dir;  By(I):= By(I)+1];
                ];
        Cursor(Bx(I), By(I));  ChOut(6, ^o);    \draw ball at its new position
        ];
    Sound(0, 3, 1);                             \delay about 1/6 second
until KeyHit;                                   \continue until a key is struck
]
Output:
           o
          o.
          .o.
         . .o.
        .o. . .
       . .o. . .
      . .o. . . .
     . .o. . .o. .
    . . .o. . . . .
   . . . . . .o. . .
  . . . . .o. .o. . .
: : : : : : : : : : : :
: : : : :o: : : : : : :
: : : :o: : : : : : : :
: : : : : : : : : : : :
: : : : :o: : : : : : :
: : : : : : : :o: : : :
: : : : : :o: : : : : :
: : :o: : :o: : : :o: :
: : : : : :o:o: : : : :
: : : : :o:o:o: : : : :
: : : : :o:o:o: : : : :
: : :o:o:o:o:o:o: : : :
.......................

Yabasic

bola$ = "0000ff"
obst$ = "000000"

maxBalls = 10
cx = 1
cy = 2
dim Balls(maxBalls, 2)

open window 600,600
window origin "ct"

maxh = peek("winheight")

REM Draw the pins:

FOR row = 1 TO 7
  FOR col = 1 TO row
    FILL circle 40*col - 20*row, 40*row+80, 10
  NEXT col
NEXT row

REM Animate
tick = 0
bolas = 0
color 0,0,255

do
	if (bolas < maxBalls) then
		if tick = 3 then
			tick = 0
			bolas = bolas + 1
			Balls(bolas, cx) = 20
			Balls(bolas, cy) = 10
		end if
		tick = tick + 1
	end if
	for n = 1 to bolas
		if Balls(n, cy) then
			color$ = right$(getbit$(Balls(n,cx),Balls(n,cy) + 10,Balls(n,cx),Balls(n,cy) + 10),6)
			if (color$ = bola$) or (Balls(n,cy) >= maxh - 15) then
				Balls(n,cy) = 0
				break
			end if
			clear fill circle Balls(n,cx),Balls(n,cy),10
			if color$ = obst$ then
				if int(ran(2)) then
					Balls(n,cx) = Balls(n,cx) - 20
				else
					Balls(n,cx) = Balls(n,cx) + 20
				end if
			end if
			Balls(n,cy) = Balls(n,cy)+10
			fill circle Balls(n,cx),Balls(n,cy),10
			wait .001
		else
			wait .001
		end if
	next n
loop

Zig

const std = @import("std");
const rand = std.rand;
const time = std.time;

const PEG_LINES = 20;
const BALLS = 10;

fn boardSize(comptime peg_lines: u16) u16 {
    var i: u16 = 0;
    var size: u16 = 0;
    inline while (i <= peg_lines) : (i += 1) {
        size += i + 1;
    }
    return size;
}

const BOARD_SIZE = boardSize(PEG_LINES);

fn stepBoard(board: *[BOARD_SIZE]u1, count: *[PEG_LINES + 1]u8) void {
    var prng = rand.DefaultPrng.init(@bitCast(time.timestamp()));

    var p: u8 = 0;
    var sum: u16 = 0;
    while (p <= PEG_LINES) : (p += 1) {
        const pegs = PEG_LINES - p;
        var i: u16 = 0;
        while (i < pegs + 1) : (i += 1) {
            if (pegs != PEG_LINES and board[BOARD_SIZE - 1 - sum - i] == 1) {
                if (prng.random().boolean()) {
                    board.*[BOARD_SIZE - 1 - sum - i + pegs + 1] = 1;
                } else {
                    board.*[BOARD_SIZE - 1 - sum - i + pegs + 2] = 1;
                }
            } else if (pegs == PEG_LINES and board[BOARD_SIZE - 1 - sum - i] == 1) {
                count.*[pegs - i] += 1;
            }
            board.*[BOARD_SIZE - 1 - sum - i] = 0;
        }
        sum += pegs + 1;
    }
}

fn printBoard(board: *[BOARD_SIZE]u1, count: *[PEG_LINES + 1]u8) !void {
    const stdout = std.io.getStdOut();
    _ = try stdout.write("\x1B[2J\x1B[1;1H");
    var pegs: u16 = 0;
    var sum: u16 = 0;
    while (pegs <= PEG_LINES) : (pegs += 1) {
        var i: u16 = 0;
        while (i < (PEG_LINES - pegs)) : (i += 1) _ = try stdout.write(" ");
        i = 0;
        while (i < pegs + 1) : (i += 1) {
            const spot = if (board[i + sum] == 1) "o" else " ";
            _ = try stdout.write(spot);
            if (i != pegs) _ = try stdout.write("*");
        }
        sum += pegs + 1;
        _ = try stdout.write("\n");
    }
    for (count) |n| {
        const num_char = [2]u8{'0' + n, ' '};
        _ = try stdout.write(&num_char);
    }
    _ = try stdout.write("\n");
}

pub fn main() !void {
    var board: [BOARD_SIZE]u1 = [_]u1{0} ** BOARD_SIZE;
    var bottom_count: [PEG_LINES+1]u8 = [_]u8{0} ** (PEG_LINES + 1);

    var i: u16 = 0;
    while (i < PEG_LINES + BALLS + 1) : (i += 1) {
        if (i < BALLS) board[0] = 1;

        try printBoard(&board, &bottom_count);
        stepBoard(&board, &bottom_count);
        time.sleep(150000000);
    }
}