Forum Discussion
Utilizing Excel's turing capabilities to create Conway's 'Game of Life'
The Background
It's been said with Lambda (and LET and a wealth of functions in 365) Excel has become 'turing-complete'.
To quote the article linked below:
"You can now, in principle, write any computation in the Excel formula language."
https://www.microsoft.com/en-us/research/blog/lambda-the-ultimatae-excel-worksheet-function/
The Challenge
I thought it would be fun to create Conway's 'Game of Life' in Excel 365 to see how far I could push things.
Conway's Game of Life - Wikipedia
The rules are simple:
A 'cell' has up to 8 adjacent cells (less if the cell is on the edge of the board). A 'neighbor' is a cell with a 1 while a 'dead' cell is empty.
An 18x18 board
Multiple iterations
Bigger boards!
more (it's relaxing to create new shapes and designs)
The Approach
My first thought was to use MAKEARRAY because I could use 'r' and 'c' coordinates and there would be no stacking. I devised a recursive function that worked for 1 iteration but failed on subsequent iterations because the use of TAKE/DROP was slowly shrinking the board!
The revised approach is essentialy a recursive MAP that uses 3 arrays: the input matrix, the 'r' array (row numbers) and the 'c' array (column numbers). It's my way of using r/c without using MAKEARRAY.
For Discussion
I welcome any improvements to the existing function and any different approaches someone may have to creating Conway's Game of Life.
Conway Lambda follows:
Conway
=LAMBDA(matrix, iterations,
IF(
iterations = 0,
matrix,
Conway(
LET(
height, ROWS(matrix),
width, COLUMNS(matrix),
r_arr, SEQUENCE(height) * SEQUENCE(, width, 1, 0),
c_arr, SEQUENCE(height, , 1, 0) * SEQUENCE(, width),
CheckNeighbors, LAMBDA(lattice, r, c,
LET(
RCx, LAMBDA(row, col,
IFERROR(CHOOSECOLS(CHOOSEROWS(matrix, row), col), 0)
),
N, RCx(r - 1, c),
NE, RCx(r - 1, c + 1),
E, RCx(r, c + 1),
SE, RCx(r + 1, c + 1),
S, RCx(r + 1, c),
SW, RCx(r + 1, c - 1),
W, RCx(r, c - 1),
NW, RCx(r - 1, c - 1),
compass, VSTACK(N, NE, E, SE, S, SW, W, NW),
neighbors, SUM(compass),
IF(
AND(lattice = 0, neighbors = 3),
1,
IF(
AND(lattice = 1, OR(neighbors = 2, neighbors = 3)),
1,
0
)
)
)
),
MAP(matrix, r_arr, c_arr, CheckNeighbors)
),
iterations - 1
)
)
)
27 Replies
- djclementsBronze Contributor
INDEX can be used with an array of row and column numbers to transform the board into a table of records for each cell and its neighbors. Since INDEX does not trigger an error when 0 is provided as the row and/or column number, a logical test is needed to handle out-of-scope references for the outer edges of the board. From there, BYROW-SUM is used to get the total number of live neighbors for each cell, and WRAPROWS returns the new board back to its original dimensions.
=IF( NOT(IterationsA), board, LET( h, ROWS(board), w, COLUMNS(board), i, TOCOL(IFNA(SEQUENCE(h),board))+{0,0,0,1,1,1,-1,-1,-1}, j, TOCOL(IFNA(SEQUENCE(,w),board))+{0,1,-1,0,1,-1,0,1,-1}, t, i*j*(i<=h)*(j<=w), REDUCE( board, SEQUENCE(IterationsA), LAMBDA(arr,num, LET( a, IF(t,INDEX(arr,i,j),0), n, BYROW(DROP(a,,1),SUM), WRAPROWS(TAKE(a,,1)*(n=2)+(n=3),w) ) ) ) ) )
Alternatively, you could EXPAND the board to add an outer border of 0's, then DROP the appropriate number of rows and/or columns to shift the entire board in each of the 8 directions, to identify all neighbors at once.
=IF( NOT(IterationsB), BigBoard, LET( h, ROWS(BigBoard)+1, w, COLUMNS(BigBoard)+1, i, EXPAND(0,h+1,,0), j, EXPAND(0,,w,0), REDUCE( BigBoard, SEQUENCE(IterationsB), LAMBDA(arr,num, LET( a, HSTACK(i,VSTACK(j,EXPAND(arr,h,w,0))), ↑, DROP(DROP(a,,1),-2,-1), ↓, DROP(DROP(a,2,1),,-1), ←, DROP(DROP(a,1),-1,-2), →, DROP(DROP(a,1,2),-1), ↖, DROP(a,-2,-2), ↙, DROP(a,2,-2), ↗, DROP(a,-2,2), ↘, DROP(a,2,2), n, ↑ + ↓ + ← + → + ↖ + ↙ + ↗ + ↘, arr*(n=2)+(n=3) ) ) ) ) )
This can also be done systematically with MAP and REDUCE, which is demonstrated in the attached workbook. I've also included two boards that loop/repeat, so you can just click and hold the spinner button to watch it go!
Cheers!
- djclementsBronze Contributor
Matrix Flow Accumulation adaptation using the same basic concept to transform the matrix into a table of records, then lambda recursion to generate a hierarchy table summarized with GROUPBY:
=LET( rng, BigFlowMatrix, h, ROWS(rng), w, COLUMNS(rng), n, SEQUENCE(h,w), i, TOCOL(IF(n,SEQUENCE(h)))+{0,0,-1,-1,-1,0,1,1,1}, j, TOCOL(IF(n,SEQUENCE(,w)))+{0,-1,-1,0,1,1,1,0,-1}, t, i*j*(i<=h)*(j<=w), val, IF(t,INDEX(rng,i,j),0), num, IF(t,INDEX(n,i,j),0), tst, DROP(val,,1)={1,2,4,8,16,32,64,128}, pId, TOCOL(IFS(tst,TAKE(num,,1)),2), cId, TOCOL(IFS(tst,DROP(num,,1)),2), fnλ, LAMBDA(me,arr,[acc], LET( acc, IF(ISOMITTED(acc),arr,acc), inc, ISNUMBER(XMATCH(DROP(arr,,1),pId,,2)), IF( OR(inc), LET( a, TRANSPOSE(FILTER(arr,inc)), b, DROP(a,1)=pId, c, HSTACK(TOCOL(IFS(b,TAKE(a,1)),2),TOCOL(IFS(b,cId),2)), me(me,c,VSTACK(acc,c)) ), GROUPBY(TAKE(acc,,1),DROP(acc,,1),ROWS,0,0) ) ) ), tbl, fnλ(fnλ,HSTACK(pId,cId)), XLOOKUP(n,TAKE(tbl,,1),DROP(tbl,,1),0,,2) )
😊
- Patrick2788Silver Contributor
Excel has come a long way in the last 6-7 years giving us multiple ways to solve problems. A 4th solution would make this a solution party!
Our approaches are similar but differ a bit with the recursive function and the use of GROUPBY to do the aggregations. I like GROUPBY because it lessens the burden on REDUCE to handle such calculations.
There is another matrix that precedes the directional matrix provided in the sample. This matrix contains elevation data which is checked with another algorithm. The algorithm is summarized here:
How Derive Continuous Flow works—ArcGIS Online | Documentation
It sounds do-able but certainly a bit of a time sink!
- PeterBartholomew1Silver Contributor
You have prompted me to have another go. I have made some minor changes to your solution such as using REDUCE rather than recursion and a DROP/TAKE combination to isolate neighbourhoods.
The fun step was then to switch REDUCE to SCAN and produce the result as an array of thunks. That allows the user to iterate the board in place or display the entire sequence of results.
Worksheet formula = LET( result, SCAN(THUNK(board), SEQUENCE(generations), Stepλ), EVALTHUNKARRλ(IF(ShowAll, result, TAKE(result,-1))) ) //A simulation of Conway's Game of Life Stepλ =LAMBDA(gridϑ, k, LET( grid, gridϑ(), life, MAKEARRAY( ROWS(grid), COLUMNS(grid), LAMBDA(r, c, LET( border?, OR(r=1, c=1, r=ROWS(grid), c=COLUMNS(grid)), // Identify neighbourhood and cell state neighbourhood, TAKE(DROP(grid, r - 2, c - 2), 3, 3), alive, INDEX(neighbourhood, 2, 2), //total the neighbours count, SUM(neighbourhood) - alive, //cell has 3 neighbors or is alive and has 2 neighbors - keep IF(border?, "x", alive * (count = 2) + (count = 3)) ) ) ), THUNK(life)) );
- Patrick2788Silver Contributor
This is a clever approach to a situation that on the surface appears to only be solvable with recursion. As I understand it:
The board is thunked immediately and provided as the initial value (It's a good way to "fit a matrix through a scalar-sized keyhole"). I like how SCAN does its work on {1;2;3...} instead of having to accept the entire board. 'k' appears to arbitrary in that it fulfills a parameter but SCAN is only concerned with the accumulated matrix. Due to the limitations of SCAN, the result must be thunked (because it can't yet be properly evaluated) and unpacked with EVALTHUNKARRλ.
I may try this approach with of few of the Wolfram rules that use a starting configuration of {0,1,0} that can be solved with recursion and a few bit functions. It's probably not needed but it's the experimentation that's fun!
- PeterBartholomew1Silver Contributor
Nice description!
The array of thunks allows a reasonable level of customisation of the layout as well. For example
= LET( resultϑ, SCAN(THUNK(board), SEQUENCE(generations), Stepλ), EVALTHUNKARRλ(IF(ShowAll, WRAPROWS(resultϑ,5), TAKE(resultϑ,-1))) )
will arrange the display to have 5 boards across each row. I also changed the spinner to increment in fives.
- Patrick2788Silver Contributor
This updated approach for 2025 uses a kernel and MAP to do the heavy lifting. I tend to err on the side of more parameters and readability.
//Kernel coordinates: x = {-1;-1;-1;0;1;1;1;0}; y = {-1;0;1;1;1;0;-1;-1}; //A simulation of Conway's Game of Life Conwayλ = LAMBDA(config,n, LET( //determine dimensions of the board h,ROWS(config), w,COLUMNS(config), //create matrices of coordinates i,SEQUENCE(h)*SEQUENCE(,w,1,0), j,TRANSPOSE(i), //determine if cell is to be kept or discarded generate,MAP(i,j,LAMBDA(i_,j_, LET( each_cell,INDEX(config,i_,j_), //generate coordinates for each cell //discard certain coordinates if cell is on perimeter of board kernel,(i_+ x>0)*(j_+y>0)*(i_+ x<=h)*(j_+y<=w), r,FILTER(i_ + x,kernel), c,FILTER(j_+ y, kernel), //total the neigbors neighbors,SUM(INDEX(config,r,c)), //cell is 0 but has exactly 3 neighbors - keep revive,(each_cell=0)*(neighbors=3), //cell is alive and has 2 or 3 neighbors - keep keep,(each_cell=1)*(neighbors=2)+(neighbors=3), IF(revive,1,IF(keep,1,0))))), IF(n=0,config,Conwayλ(generate,n-1))))
- PeterBartholomew1Silver Contributor
INDEX was far from my first choice too, but I eventually settled for testing row and column numbers for 0 and n+1. I also tested Riny_van_Eekelen 's start and at iteration 4 it returned the same shape but displaced.
Your original pattern became static at iteration 50. An advantage of recursion is that one could terminate the calculation there whereas REDUCE bashed on for the full 100 that I had set.
- Riny_van_EekelenPlatinum Contributor
Patrick2788 Interesting. I remember doing this in machine code on my Comodore64 in the late 70's. Especially the famous 'Slider' figure that just continues to in a sequence of repeating patterns. It can be seen in the wiki link you provided.
The start is like this:
The 2nd generation becomes:
And the third should be like below though the red cells do not become alive in your formula:
Stepping back to the previous state and highlighting these two red cells, and it's clear that they should become alive in the next generation as the both have three neighbors that are alive at the time. Or have I misunderstood?
- Patrick2788Silver ContributorThank you for taking a look. I've determined the 'SW' check is missing. The formula sees the cell as 0 with the neighbors for the cell at 2 so it's not making it live. If I'm able to have some uninterrupted time at work I may be able to fix it.
- Patrick2788Silver Contributor
I've updated the Conway Lambda in the first post and replaced the workbook. The issue was IFERROR was resolving to "" when it should have been 0.
In re:
Stepλ(board) =LAMBDA(r, c, LET( n, 18, state, INDEX(board, r, c), onboard, SIGN(MOD(r + δr, n + 1)) * SIGN(MOD(c + δc, n + 1)), count, SUM(IF(onboard, INDEX(board, r + δr, c + δc), 0)) - state, IF(state, SUM(SIGN(count = {2, 3})), SIGN(count = 3)) ) )
This a really clever use of the functions available! I like how you've really simplified the logic by approaching it this way. My use of IF-AND-OR is slowing calculations a bit.
Below is my MAKEARRAY version. I've compared the results up to 20 iterations against my original and Peter's solution and they're identical.
'Conway with MAKEARRAY =LAMBDA(matrix,iterations,IF( iterations = 0, matrix, Conway( LET( height, ROWS(matrix), width, COLUMNS(matrix), CheckNeighbors, LAMBDA(r, c, LET( grid_val, INDEX(matrix, r, c), ext_rows, SEQUENCE(3, , r - 1), ext_cols, SEQUENCE(, 3, c - 1), Square3x3, INDEX(matrix, ext_rows, ext_cols), neighbors, IF( OR(r = 1, c = 1, r = height, c = width), 0, SUM(Square3x3) - grid_val ), IF( AND(grid_val = 0, neighbors = 3), 1, IF(AND(grid_val = 1, OR(neighbors = 2, neighbors = 3)), 1, 0) ) ) ), MAKEARRAY(height, width, CheckNeighbors) ), iterations - 1 ) ))
My goal was to simplify things - mainly the directional checks. For this version I'm supplying the Lambda with a 'padded' matrix:
With MAKEARRAY I then 'offset' from the 'grid-val' with INDEX and the rest becomes simpler.
- PeterBartholomew1Silver Contributor
I too had a one-step success, followed by failure. I started by just exploring a single step of the process and came up with
Stepλ "Calculates outturn for a single cell" = LET( count, SUM(OFFSET(cell, -1, -1, 3, 3)) - cell, IF(cell, SUM(SIGN(count = {2, 3})), SIGN(count = 3)) )
For the entire board
= MAP(start, Stepλ)
worked fine but
= REDUCE(start, SEQUENCE(2), LAMBDA(board,k, MAP(board, Stepλ)))
failed because OFFSET is a range operator and, at step 2, I was offering it an array. Back to the drawing board! I would probably stick with REDUCE but it is back to the start with OFFSET.
At the moment I see myself as going with MAKEARRAY in order to generate indices and
= SUM(INDEX(start,r+δr,c+δc)) - INDEX(start,r,c) δr = {1;0;-1} δc = {1,0,-1}
Still some edge conditions to sort, though.
- Patrick2788Silver Contributor
The big problem I encounterd with INDEX (and why you see my odd CHOOSECOLS-CHOOSEROWS concoction) is what happens when 'r' and/or 'c' is 0. I'd rather have INDEX fail in such a scenario so I went with the two nested functions. TAKE/DROP seem to make sense but were slowly eating the board on each iteration.