A webcam points at a physical Connect 4 board and the program works out the state of the game: where every disc is, which colour it is, and whether anybody has won. The name comes from the goal, letting someone play the real board against a computer, or against a remote opponent, without the computer ever being told the moves.
Finding the board
Each frame goes through a Hough circle transform tuned to the disc radius, which finds the 42 holes in the board whether they are empty or filled. Overlapping detections are dropped, and the survivors are sorted into six rows with k-means clustering on their y coordinates, then by x within each row, which gives a 6 x 7 grid regardless of a bit of camera tilt. If the count isn't 42 the frame is rejected rather than guessed at.
Reading the discs
The pixel at the centre of each circle is classified as red, yellow or empty with simple RGB thresholds, which turned out to be good enough under room lighting once the thresholds were widened to cope with the glossy plastic. The board state is a 6 x 7 array of R, Y and O, and a win check scans rows, columns and both diagonals for four in a line.
Seeing what it sees
The viewer tiles four images: the raw frame, the detected circles, the circles filled with the colour that was sampled at their centre, and the reconstructed board. Watching all four side by side was the fastest way to find out why a detection went wrong, usually a reflection or a hand in shot. Helper classes let you crop a region of interest with the mouse and scale the input down so the Hough transform runs at a sensible frame rate, and a notebook holds the experiments that set the parameters.
circles = cv2.HoughCircles(gray, cv2.HOUGH_GRADIENT, dp=1, minDist=average_radius,
param1=200, param2=20,
minRadius=piece_radius - radius_delta,
maxRadius=piece_radius + radius_delta)
kmeans = KMeans(n_clusters=6, n_init=10) # six rows
rows = kmeans.fit_predict(circles[:, 1].reshape(-1, 1))