A ‘Bean Machine’, also known as Galton Board, is a simple but appealing way to watch statistics at work. On their way from top to bottom, all (red) ‘beans’ will hit a fixed number N of (green) obstacles before ending up in one of the N+1 collecting bins. A bean’s path is the result of N random choices between left and right at every collision. In a perfect machine, both directions have an equal 50% chance of being chosen by the bean when it hits an obstacle.
When the number of processed beans increases, the pattern of the collected beans will approximate a binomial distribution curve.
I wrote a small Arduino sketch for simulating a Bean Machine on a 480×320 TFT display. Then I decided to add some ‘audio’ by sounding a short beep at every collision. Its frequency depends on the current distance from the center of the machine (the average of the distribution), so different bean paths will have different ‘melodies’.
Here’s a short video impression:
As you can see, N=9 in my sketch. The process stops when one of the 10 bins is completely filled with beans:
Here’s the sketch. All code for drawing the machine is in setup(). Two functions take care of simulating a dropping bean (drop_vert and drop_diag). Every loop() cycle processes one bean.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 |
/* Galton Box simulation on a 480x320 TFT screen (with piezo buzzer for acoustic effect) */ #include <SPI.h> #include "Adafruit_GFX.h" #include "Adafruit_HX8357.h" #define TFT_CS 10 #define TFT_DC 9 #define TFT_RST 8 // Colors used: #define BLACK 0x0000 #define RED 0xF800 #define LIGHTGREY 0xC618 Adafruit_HX8357 tft = Adafruit_HX8357(TFT_CS, TFT_DC, TFT_RST); const int nr=10; // nnumber of bins int collect[nr]; // will hold number of collected balls per bin int Xb,Yb,row,j,dx,dy,radius,xl,xr,total,from_x,from_y,to_x,to_y; float rsq2; // holds radius*sqrt(2) void setup() { randomSeed(analogRead(0)); tft.begin(HX8357D); tft.setRotation(2); // portrait mode tft.fillScreen(HX8357_BLACK); // some geometric parameters Xb=160; Yb=40; dx=16; dy=dx; radius=5; rsq2=radius*sqrt(2); xl=Xb-rsq2; xr=Xb+rsq2; total = 0; for (j=0;j<nr;j++) collect[j]=0; // Draw entry: tft.drawFastVLine(xl, 0, Yb, LIGHTGREY); tft.drawFastVLine(xr, 0, Yb, LIGHTGREY); // Draw left border: tft.drawLine(xl-1, Yb, xl-(nr-1)*dx, Yb+(nr-1)*dy, LIGHTGREY); // Draw right border: tft.drawLine(xr+2, Yb, xr+(nr-1)*dx, Yb+(nr-1)*dy, LIGHTGREY); for (row=0;row<nr;row++) { for (j=-row;j<=row;j=j+2) { xl=Xb+j*dx-rsq2; xr=Xb+j*dx+rsq2; if (row == nr-1) { // lowest row // Draw collecting bin for this position: tft.drawFastVLine(xl, Yb+(nr-1)*dy, 479-radius/2-(Yb+(nr-1)*dy), LIGHTGREY); tft.drawFastVLine(xr, Yb+(nr-1)*dy, 479-radius/2-(Yb+(nr-1)*dy), LIGHTGREY); } else { // Draw two diagonal channels down from this position: int h1=Yb-rsq2+(row+1)*dy; int h2=Xb+rsq2+(j-1)*dx; int h3=Xb-rsq2+(j+1)*dx;; // 1. diagonal left: if (j != -row) tft.drawLine(xl, Yb+row*dy, Xb+(j-1)*dx, h1, LIGHTGREY); // left tft.drawLine(Xb+j*dx, Yb+rsq2+row*dy, h2, Yb+(row+1)*dy, LIGHTGREY); // right // 2. diagonal right: if (j != row) tft.drawLine(xr, Yb+row*dy, Xb+(j+1)*dx, h1, LIGHTGREY); // right tft.drawLine(Xb+j*dx, Yb+rsq2+row*dy, h3, Yb+(row+1)*dy, LIGHTGREY); // left } } } } void drop_vert(int from_x, int from_y, int to_y, int ball_rad) { int steps = to_y-from_y; //to_y > from_y because we're dropping balls for (int s=0;s<steps;s++) { tft.fillCircle(from_x, from_y+s, ball_rad, BLACK); tft.fillCircle(from_x, from_y+s+1, ball_rad, RED); } } void drop_diag(int from_x, int from_y, int to_x, int to_y, int ball_rad) { int steps = to_y-from_y; //to_y > from_y because we're dropping balls int factor; // shows direction if (to_x>from_x) { factor = 1; } else { factor = -1; } for (int s=0;s<steps;s++) { tft.fillCircle(from_x+factor*s, from_y+s, ball_rad, BLACK); tft.fillCircle(from_x+factor*(s+1), from_y+s+1, ball_rad, RED); } } void loop() { // new ball: from_x=Xb; from_y=radius; int nrp=0; // nrp = Next Row Position: index of the target position in the next row tft.fillCircle(from_x, from_y, radius-1, RED); delay(500); tft.setTextColor(LIGHTGREY, BLACK); tft.setCursor(240, 20); tft.setTextSize(2); tft.print(total+1); delay(500); // Start with vertical drop from entry point to first row drop_vert(from_x,from_y,Yb,radius-1); delay(100); from_y=Yb; // then randomly drop down from row to row for (row=1;row<nr;row++) { nrp = nrp+random(0,2); // coordinates target position: to_x=Xb+(2*nrp-row)*dx; to_y=Yb+row*dy; drop_diag(from_x,from_y,to_x,to_y,radius-1); // play a short beep at each collision; frequency indicates current distance to centre tone(6, 500+abs(nrp-row/2)*100, 4); delay(100); from_x=to_x; from_y=to_y; } // keep track of number of balls that have been processed total++; // Finally, drop vertically within the bin and become new top of the stack to_y=479-(radius-1)-2*collect[nrp]*(radius-1); if (to_y-from_y>2*radius) { drop_vert(from_x,from_y,to_y,radius-1); tone(6,100,5); } else { while(1); // One of the bins is full: ready } collect[nrp]=collect[nrp]+1; } |