I’m currently working on a university project where I have to implement the Oware (or Awele) game in Common Lisp, including a basic AI using the Minimax algorithm with Alpha Beta pruning. As someone new to Lisp, I quickly ran into a bug that had me scratching my head for a while.
I’ll walk you through the error I faced, how I debugged it, and some extra improvements I added to make my code easier to use and understand.
The goal of my project is to simulate the game Oware with an AI opponent that uses Minimax with Alpha-Beta pruning. The board is simply a list of integers representing the number of seeds in each pit, and the AI evaluates the best move to make based on simulated outcomes.
Sounds simple in theory but I hit a wall with a mysterious type error during testing.
The Error I Encounter
When I tried to run my alpha-beta
function, I got the following error:
Argument Y is not a NUMBER: (5 5 5 5 4 4 4 4 4 4 4)
This message was confusing at first. It basically says that somewhere in my code, I’m trying to use a list (like (5 5 5 5 4 4 ...)
) where a number is expected probably in a +
, comparison, or arithmetic expression.
The Bug Was Hiding
Looking through my code, I found the suspicious line:
(+ n-score (cdar l-board-score))
At this point, l-board-score
comes from:
(multiple-value-list (make-move move (copy-seq board) player))
I assumed (cdar l-board-score)
would return the captured score. But here’s the issue: cdar
means (cdr (car l-board-score))
. And (car l-board-score)
was actually the new board, which is a list! So I was trying to +
a number with a list. That explains the error.
How I Fix It
I remembered that Lisp has a clean way to handle multiple values: multiple-value-bind
.
So I rewrote the block like this:
(multiple-value-bind (board2 captured)
(make-move move (copy-seq board) player)
...)
This way, I correctly separate the new board and the captured seeds instead of wrapping them into a list and unpacking them incorrectly.
The Correct Alpha Beta Function
Here’s the working and updated version of my alpha-beta
function, with the bug fixed and extra enhancements for practice:
(defun alpha-beta (player board n-score s-score alpha beta depth)
(if (or (= depth 0) (game-over player board n-score s-score))
(eval-s player n-score s-score)
(let* ((moves (valid-list player board))
(b-move (first moves)))
(loop for move in moves
do (multiple-value-bind (board2 captured)
(make-move move (copy-seq board) player)
(let ((new-n-score (if (eq player 'north) (+ n-score captured) n-score))
(new-s-score (if (eq player 'south) (+ s-score captured) s-score)))
(let ((val (- (alpha-beta (opponent player)
board2 new-n-score new-s-score
(- beta) (- alpha) (1- depth)))))
(when (> val alpha)
(setf alpha val)
(setf b-move move)))))
until (>= alpha beta))
(values alpha b-move))))
Extra Practice Feature I Added
To make my project more robust and easier to debug, I added the following helper functions and enhancements.
Move History Tracking
Helps track the AI’s decision path:
(defun alpha-beta (... &optional path)
...
(let ((best-path nil))
(loop for move in moves
do (multiple-value-bind (board2 captured)
...
(let ((val (- (alpha-beta ... (cons move path)))))
...
(setf best-path (cons move path)))))
(values alpha b-move best-path)))
Evaluation Logging
Useful for tracing how the score is evaluated:
(defun eval-s (player n-score s-score)
(let ((score (- n-score s-score)))
(format t "~%Eval: N=~A S=~A => ~A~%" n-score s-score score)
score))
Board Display
Quick helper to print the board state:
(defun display-board (board)
(format t "~%North: ~A~%" (subseq board 0 6))
(format t "South: ~A~%" (subseq board 6 12)))
Timing Benchmark
Tracks how long the AI takes to calculate:
(defun run-ai ()
(let ((start (get-internal-real-time)))
(multiple-value-bind (val move)
(alpha-beta 'north *board* 0 0 -1000 1000 6)
(format t "Best move: ~A with value: ~A~%" move val)
(format t "Time taken: ~A ms~%" (/ (- (get-internal-real-time) start) 1000)))))
Summary of the Fix
Issue | Fix |
---|---|
cdar of board was a list | Used multiple-value-bind to get values directly |
Assumed numeric return from a list | Properly unpacked multiple values from make-move |
Code was hard to debug | Added logging, board printing, and timing helpers |
Final Thought
Debugging in Lisp as a beginner was intimidating at first, especially with vague errors like “not a number.” But breaking down the error message, understanding how multiple values are handled, and carefully inspecting each expression helped me find the root cause.
This experience reminded me how important it is to not just copy patterns blindly (car
, cdr
, cdar
…) but to deeply understand what structure you’re working with. Once I fixed the return handling and added some debug features, everything became much more manageable and even fun.