For the complete Mojo documentation index, see llms.txt. Markdown versions of all pages are available by appending .md to any URL (e.g. /docs/manual/basics.md).
Get started with Mojo
Get started with Mojo by building Conway's Game of Life, a simulation in which cells live, die, and reproduce based on the state of their neighbors.
This tutorial walks you through the steps to build a simple version of the game. It should take you about 45-60 minutes to complete. Don't feel you need to rush it.
Whether you arrive from, say, C++ or Python, you'll encounter unfamiliar syntax like transfer operators and compile-time variables. Read the Checkpoint items to map these new features to concepts you already know.
Run the code in your terminal and watch the grid evolve over time. As you build the game, you'll learn the Mojo syntax you need to start writing programs of your own.
Game state
Conway's Game of Life runs on a two-dimensional grid. Each cell is either
alive or inactive. You'll use 1 for live cells and 0 for inactive
cells. Count the neighbors around each cell by adding their values to a
running sum.
Create life.mojo. Build an 8 x 8 grid containing a
glider:
def main():
var count: Int = 64
var num_cols: Int = 8
var glider_grid = List[Int](length=count, fill=0)
# Convert (x, y) coordinates to a linear index in the grid
var to_index = lambda (x: Int, y: Int) -> Int: y * num_cols + x
# Set up the grid
for coord in [(0, 1), (1, 2), (2, 0), (2, 1), (2, 2)]:
glider_grid[to_index(coord[0], coord[1])] = 1
# Print the grid
for index in range(count):
print("X" if glider_grid[index] else ".", end="")
if index % num_cols == (num_cols - 1): print()
Run the program to see the initial glider configuration:
mojo life.mojo
Output:
..X.....
X.X.....
.XX.....
........
........
........
........
........
Checkpoint
- All variable declarations like
countandnum_colsstart withvar. varbindings are mutable by default.- The
to_indexlambda expression defines a small local function that converts coordinates to a list index. Lambdas are short anonymous functions that evaluate a single expression.
When assigning contents, a list expression sets the values:
var values: List[Int] = [12, -7, 64] # This is a list expression
In type names, square brackets supply compile-time parameters:
List[Int] # List is a standard library-supplied type
Grid[8, 8] # Grid is a custom type
Parentheses supply run-time arguments:
print(value)
Grid[8, 8]()
Add reusable printing
Move the display loop to a reusable function. Place this above main():
def print_grid(grid: List[Int], num_cols: Int):
for index in range(len(grid)):
print("X" if grid[index] else ".", end="")
if index % num_cols == (num_cols - 1): print()
Replace the print loop with print_grid(glider_grid, num_cols) and run.
Improve performance by replacing individual prints with a single string:
def print_grid(grid: List[Int], num_cols: Int):
var grid_str = ""
for index in range(len(grid)):
grid_str += "X" if grid[index] else "."
if (index % num_cols == (num_cols - 1) and
index != len(grid) - 1):
grid_str += "\n"
print(grid_str)
When running the program with larger grids and many generations, this approach is faster than printing each cell individually. You print once per frame instead of once per cell.
Call print_grid() from main() and confirm the output:
# Print the grid
print_grid(glider_grid, num_cols)
Add lookups
Make a few more changes in place in life.mojo:
comptime count: Int = 64
comptime num_cols: Int = 8
# Transform coordinates to a linear index
comptime to_index = lambda (x: Int, y: Int) -> Int: (
y * num_cols + x
)
# Transform linear index to coordinates
comptime to_coord = lambda (i: Int) -> Tuple[Int, Int]: (
(i % num_cols, i // num_cols) # `//` is flooring divide
)
def print_grid(grid: List[Int]):
for index in range(len(grid)):
print("X" if grid[index] else ".", end="")
if index % num_cols == (num_cols - 1): print()
def main():
var glider_grid: List[Int] = List[Int](length=count, fill=0)
# Set up the grid
for coord in [(0, 1), (1, 2), (2, 0), (2, 1), (2, 2)]:
glider_grid[to_index(coord[0], coord[1])] = 1
# Print the grid
print_grid(glider_grid)
Now you can convert coordinates to indices and indices to coordinates.
Checkpoint
comptimedeclarations are evaluated at compile time before the code runs. This lets Mojo optimize and generate efficient machine code.- The flooring divide operator (
//) used here performs integer division, rounding towards negative infinity. For integer types,/returns an integer, rounding towards zero. - You've moved all the constants out from
main()tocomptimedeclarations, making them available throughout this file. - You've created two lambda expressions that convert between coordinates and indices.
- You've removed the second argument from
print_grid().
Try this
In a separate source file, declare the number of rows as a comptime constant and compute the count at compile time.
Remove the new source after verifying your solution is correct. You will always need two of these three items: the number of rows, the number of columns, and the total count of cells.
Define a Grid type
Revise your code again, into a new file called grid.mojo. You're
creating a new type, called a struct:
struct Grid[num_cols: Int, num_rows: Int]:
var cells: List[Int]
var count: Int
def __init__(out self):
self.count = Self.num_cols * Self.num_rows
self.cells = List[Int](length=self.count, fill=0)
def main():
var glider_grid = Grid[8, 8]()
__init__(out self) initializes a new Grid. Every field must receive a
value before the initializer returns.
Here, count comes from the grid dimensions, and cells starts as a list
of zeros.
Checkpoint
-
Griduses both compile-time parameters and runtime fields. -
Self.num_colsandSelf.num_rowsbelong to the parameterized type. -
self.cellsandself.countbelong to oneGridinstance. -
This
Gridneeds custom initialization, so it defines__init__(). Normally, if an initializer just assigns arguments directly to fields, add@fieldwise_initto the struct instead. Mojo generates that initializer for you. For example, in an alternate implementation you might define all three core measurements as fields in your struct rather than synthesize one from the other two:@fieldwise_initstruct Grid:var cells: List[Int]var count: Intvar num_cols: Intvar num_rows: Int
Add grid operations
Put the following content into your Grid struct:
comptime to_index = lambda (x: Int, y: Int) -> Int: (
y * Self.num_cols + x
)
comptime to_coord = lambda (i: Int) -> Tuple[Int, Int]: (
(i % Self.num_cols, i // Self.num_cols)
)
def print_grid(self):
var grid_str = ""
for index in range(len(self.cells)):
grid_str += "X" if self.cells[index] else "."
if (index % Self.num_cols == (Self.num_cols - 1) and
index != len(self.cells) - 1):
grid_str += "\n"
print(grid_str)
def __setitem__(mut self, coord: Tuple[Int, Int], value: Int):
self.cells[Self.to_index(coord[0], coord[1])] = value
def __getitem__(self, coord: Tuple[Int, Int]) -> Int:
return self.cells[Self.to_index(coord[0], coord[1])]
Checkpoint
Self.to_index()accesses a type member. The lambdato_index()belongs toGrid. UppercaseSelfrefers to the type.- Instance methods take
selfas their first argument. Lowercaseselfrefers to an instance. mut selfallows a method to change the instance.__getitem__()and__setitem__()define indexing behavior, so aGridcan usegrid[coord]notation.
Don't add this anywhere. It's a preview of how this all works from the call site:
# Set up the grid
for coord in [(0, 1), (1, 2), (2, 0), (2, 1), (2, 2)]:
glider_grid[coord] = 1 # Uses indexing with `__setitem__()`
Import Grid
Remove main() from grid.mojo, save it, and import your new type into
life.mojo:
from grid import Grid # Separate concerns into separate files
def main():
var glider_grid = Grid[8, 8]()
# Set up the grid
for coord in [(0, 1), (1, 2), (2, 0), (2, 1), (2, 2)]:
glider_grid[coord] = 1
# Print the grid
glider_grid.print_grid()
Run it and confirm everything works as expected.
Be random
Gliders are terrific for validating code, but random values produce
great animations. Set up your seed in main():
from std.random import seed
from grid import Grid
def main():
seed()
# ...
Mojo won't allow runtime statements at global scope, so you can't call
seed() there. Instead, seed the RNG from executable code, such as
main() or another function. You only need to seed once: seeding sets the
state of a single PRNG shared across threads.
Add protection
Filling every cell independently gives you random static. It works, but it doesn't produce especially interesting Game of Life patterns. Instead, build a few random clumps of live cells.
To start, add checks to your setter in grid.mojo. A coordinate check
function helps:
def is_valid_coord(self, coord: Tuple[Int, Int]) -> Bool:
return (
not (
coord[0] < 0
or coord[0] >= Self.num_cols
or coord[1] < 0
or coord[1] >= Self.num_rows
)
)
def __setitem__(mut self, coord: Tuple[Int, Int], value: Int):
if not self.is_valid_coord(coord): return # no op
self.cells[Self.to_index(coord[0], coord[1])] = value
You can add a check to the getter, too. Instead of returning a made-up
value, raise an error or abort the process. The best approach tests the
coordinates before indexing (if self.is_valid_coord(coord):) to
avoid indexing errors instead of adding no-op workarounds.
Introducing errors
This version of __getitem__() raises an error when coordinates aren't
valid. Add raises to the signature before the arrow, and call raise:
def __getitem__(self, coord: Tuple[Int, Int]) raises -> Int:
if not self.is_valid_coord(coord):
raise String(t"Invalid coordinate: ({coord[0]}, {coord[1]})")
return self.cells[Self.to_index(coord[0], coord[1])]
A TString starts with t" and creates a template format. print()
statements automatically convert to strings, but everywhere else explicitly
call String().
Once __getitem__() raises, every caller must either raise or handle
the error using Mojo's try/except error handling.
For now, revert your changes and let __getitem__() handle invalid
coordinates as it did before.
Construct the random grid
Add a static method that constructs a random Grid:
from std.random import random_si64 # Add this import to grid.mojo
# and inside Grid:
@staticmethod
def random_grid(clumps: Int = 2) -> Self:
var grid = Self()
for _ in range(clumps):
var idx = Int(random_si64(0, Int64(grid.count) - 1))
var x, y = Self.to_coord(idx)
grid[(x, y)] = 1 # Fill index cell
for dx in range(-1, 2): # -1, 0, or 1
for dy in range(-1, 2):
if not grid.is_valid_coord((x + dx, y + dy)): continue
if random_si64(0, 3) > 0: continue # 75% skip
grid[(x + dx, y + dy)] = 1
return grid^
By default, random_grid() builds two clumps when called without arguments
(clumps: Int = 2). The default value follows the equal sign.
A @staticmethod belongs to the type and not an individual instance. Call
it through the Grid type:
print("\nRandom grid:\n")
var random_grid = Grid[8, 8].random_grid()
random_grid.print_grid()
You don't have to add this code except to test it. You can remove it afterwards.
Checkpoint
- This static method still needs
Gridparameters, namely 8 and 8. - Each neighboring cell has a 25% chance of becoming live. Adjust to your preference.
random_si64()returns anInt64. Cast the value toInt.- The clump loop variable is
_, the discard pattern. Use this when you don't care about the value it produces. - A
^sigil transfers the newly initializedGrid, avoiding a copy. Transfer means changing ownership, handing the value to the new owner. By skipping a copy, you avoid the time and memory costs of duplicating data.
Evolve
In life.mojo, cut out the glider grid and remove the print statements.
Next, you'll start making the grid change over time. Start by adding a
separate next_cells list to Grid and initialize it:
struct Grid[num_cols: Int, num_rows: Int]:
var cells: List[Int] # Holds the current generation
var next_cells: List[Int] # Holds the next generation
var count: Int
def __init__(out self):
self.count = Self.num_cols * Self.num_rows
self.cells = List[Int](length=self.count, fill=0)
self.next_cells = List[Int](length=self.count, fill=0)
Conway's Game of Life applies three rules to every cell:
- A live cell stays alive with two or three live neighbors.
- An inactive cell becomes alive with exactly three live neighbors.
- Every other cell is inactive in the next generation.
The updated cells are stored in the next_cells list. They won't affect
math for the previous generation.
Each cell has eight neighbors: the cells in the 3 x 3 square around it, excluding the cell itself.
Add these methods to Grid.
evolve_cell() determines the state for the next generation of a single
cell based on its neighbors:
def evolve_cell(mut self, i: Int):
var is_live = Bool(self.cells[i])
self.next_cells[i] = 0
# Count the neighbors
var ncount = -1 if is_live else 0 # Exclude self from the count
var x, y = Self.to_coord(i)
for dx in range(-1, 2):
for dy in range(-1, 2):
var nx = x + dx
var ny = y + dy
ncount += self.cells[Self.to_index(nx, ny)]
# Live cell stays alive with two or three live neighbors
if is_live and (ncount == 2 or ncount == 3):
self.next_cells[i] = 1
# Inactive cell becomes alive with exactly three live neighbors
elif not is_live and ncount == 3:
self.next_cells[i] = 1
evolve() updates the entire grid to the next generation by calling
evolve_cell() for each index:
def evolve(mut self):
for i in range(self.count):
var x, y = Self.to_coord(i)
# Edges are excluded from evolution and will always go inactive.
if (x == 0 or y == 0 or
x == Self.num_cols - 1 or y == Self.num_rows - 1):
self.next_cells[i] = 0 # Edges go inactive
continue
self.evolve_cell(i)
# Swap the current and next cell states
var tmp = self.cells^ # Transfer
self.cells = self.next_cells^ # Transfer
self.next_cells = tmp^ # Every cell is written, so this is safe
Checkpoint
- The algorithm progresses in integer order, excluding edges.
- Live cells offset by -1, to exclude them from the count.
- Mojo's "ternary" has no
? :syntax. Use the Python-styleif-elseexpression instead:-1 if is_live else 0.
Run the simulation
Add a loop so you can watch the grid evolve in the terminal. Here's the
final life.mojo:
from std.random import seed
from grid import Grid
from std.time import sleep
def main():
comptime gridw: Int = 80
comptime gridh: Int = 20
comptime grid_count: Int = 400
seed()
var grid = Grid[gridw, gridh].random_grid(grid_count)
while True:
for gen in range(100):
print(t"\033[H\033[J\nGeneration: {gen}{' ' * 4}")
grid.evolve(); grid.print_grid()
sleep(0.1)
grid = Grid[gridw, gridh].random_grid(grid_count)
Checkpoint
comptimedeclarations let you set constants.- The odd characters in the print statement are ANSI escape codes. You'll see the updates generation-by-generation. Keep your terminal at a minimum of 80x24 for best results.
- Mojo uses semicolons to separate statements, not to end them. One statement per line is the usual form. This listing pairs them to stay compact.
Your first day? Try these
- To use AI coding assistants with Mojo, see our AI skills guide for using the latest up-to-date language know-how.
- Our Mojo language reference section provides a concise reference for syntax, keywords, and more.
- You can download our cheat sheets for printable reference cards that unify entire concepts.
- Mojo Quest is a web-based game where you solve coding challenges to practice Mojo syntax.
Final code
View the complete grid.mojo
from std.random import random_si64
struct Grid[num_cols: Int, num_rows: Int]:
var cells: List[Int] # Holds the current generation
var next_cells: List[Int] # Holds the next generation
var count: Int
def __init__(out self):
self.count = Self.num_cols * Self.num_rows
self.cells = List[Int](length=self.count, fill=0)
self.next_cells = List[Int](length=self.count, fill=0)
comptime to_index = lambda (x: Int, y: Int) -> Int: (
y * Self.num_cols + x
)
comptime to_coord = lambda (i: Int) -> Tuple[Int, Int]: (
(i % Self.num_cols, i // Self.num_cols)
)
def print_grid(self):
var grid_str = ""
for index in range(len(self.cells)):
grid_str += "X" if self.cells[index] else "."
if (
index % Self.num_cols == (Self.num_cols - 1)
and index != len(self.cells) - 1
):
grid_str += "\n"
print(grid_str)
def is_valid_coord(self, coord: Tuple[Int, Int]) -> Bool:
return not (
coord[0] < 0
or coord[0] >= Self.num_cols
or coord[1] < 0
or coord[1] >= Self.num_rows
)
def __setitem__(mut self, coord: Tuple[Int, Int], value: Int):
if not self.is_valid_coord(coord):
return # no op
self.cells[Self.to_index(coord[0], coord[1])] = value
def __getitem__(self, coord: Tuple[Int, Int]) -> Int:
return self.cells[Self.to_index(coord[0], coord[1])]
@staticmethod
def random_grid(clumps: Int = 2) -> Self:
var grid = Self()
for _ in range(clumps):
var idx = Int(random_si64(0, Int64(grid.count) - 1))
var x, y = Self.to_coord(idx)
grid[(x, y)] = 1 # Fill index cell
for dx in range(-1, 2): # -1, 0, or 1
for dy in range(-1, 2):
if not grid.is_valid_coord((x + dx, y + dy)):
continue
if random_si64(0, 3) > 0:
continue # 75% skip
grid[(x + dx, y + dy)] = 1
return grid^
def evolve_cell(mut self, i: Int):
var is_live = Bool(self.cells[i])
self.next_cells[i] = 0
# Count the neighbors
var ncount = -1 if is_live else 0 # Exclude self from the count
var x, y = Self.to_coord(i)
for dx in range(-1, 2):
for dy in range(-1, 2):
var nx = x + dx
var ny = y + dy
ncount += self.cells[Self.to_index(nx, ny)]
# Live cell stays alive with two or three live neighbors
if is_live and (ncount == 2 or ncount == 3):
self.next_cells[i] = 1
# Inactive cell becomes alive with exactly three live neighbors
elif not is_live and ncount == 3:
self.next_cells[i] = 1
def evolve(mut self):
for i in range(self.count):
var x, y = Self.to_coord(i)
# Edges are excluded from evolution and will always go inactive.
if (
x == 0
or y == 0
or x == Self.num_cols - 1
or y == Self.num_rows - 1
):
self.next_cells[i] = 0 # Edges go inactive
continue
self.evolve_cell(i)
# Swap the current and next cell states
var tmp = self.cells^ # Transfer
self.cells = self.next_cells^ # Transfer
self.next_cells = tmp^ # Every cell is written, so this is safe
View the complete life.mojo
from std.random import seed
from grid import Grid
from std.time import sleep
def main():
comptime gridw: Int = 80
comptime gridh: Int = 20
comptime grid_count: Int = 400
seed()
var grid = Grid[gridw, gridh].random_grid(grid_count)
while True:
for gen in range(100):
print(t"\033[H\033[J\nGeneration: {gen}{' ' * 4}")
grid.evolve()
grid.print_grid()
sleep(0.1)
grid = Grid[gridw, gridh].random_grid(grid_count)