D
D
I
I
G
G
I
I
T
T
U
U
L
L
P
P
Tech Solutions
|
Game Dev
|
Digital Art
TECH SOLUTIONS
Check out some of my past freelance work and personal projects

Naylor Love Sign-in Monitor
This sign in monitor was created to help Naylor Love keep track of their employee sign in times on site. The turnstiles would automatically send data to the monitor which would then let the site managers filter and easily see who was on site at what time.
View Project
Interpreted Programming Language
I developed an Abstract Syntax Tree Interpreted language in Rust to further solidify my programming knowledge. Although I wouldn't recommend using it for anything practical, I learnt a lot about programming languages and useful design patterns.
View Project
Resonate
I led the development of an experimental generative art project that would create abstract pieces of art based on a user selected word.
View Project
FLUF Character Generator
During the development of the FLUF NFT collection, I created the generation algorithm that was used to generate the 10,000 unique characters. We were one of the first to achieve such high quality assets in the NFT space due to a combination of manual curation and complex algorithms.
View ProjectGAME DEV
Below are some of my more recent Game Development projects, all developed solo in Unity

Kiwi Ingenuity
Kiwi Ingenuity is an upcoming, open world 3D puzzle platformer game and is my most ambitious project to date. I am planning on a Steam release in 2026.

Little Space Game
Little Space Game is a game where the player explores an infinite procedural galaxy filled with mineable planets and resources. Build your rocket from unlockable parts and construct machinery on planets to expand your reach through the universe.

Hex Defence
Hex Defense is a tower defence game set on a procedurally generated hexagonal grid where you fight endless waves of alien enemies.

Fuse Cube
Fuse Cube is a small mobile game that I developed and released on the Google Play Store. It is a 3D take on the classic 2048 puzzle game.
DIGITAL ART
Explore some highlights from my visual work and digital art pieces











































































Pokemon City
One of my most recent works was a commision piece for The Game Tree NZ. They asked me to design and illustrate a digital banner for display on their Social Media and website.
CONTACT
Get in touch if you want to work together or just say hi!
/// The parser struct handles incoming token streams and converts them into statements and
/// expressions
pub struct Parser {
tokens: Vec<Token>,
current: usize,
error_handler: Rc<RefCell<ErrorHandler>>,
}
impl Parser {
pub fn new(tokens: Vec<Token>, error_handler: Rc<RefCell<ErrorHandler>>) -> Self {
Self { tokens, current: 0, error_handler }
}
/// Method called to parse the tokens
pub fn parse(&mut self) -> Vec<Stmt> {
let mut statements = Vec::new();
while !self.is_at_end() {
match self.declaration() {
Ok(stmt) => statements.push(stmt),
Err(e) => {
error!(self, e);
self.synchronize();
},
}
}
statements
}
/// ================================ STATEMENTS
/// Parse a declaration
fn declaration(&mut self) -> Result<Stmt, Error> {
if self.match_token(&[TokenType::Var]) {
return self.variable_declaration();
}
if self.match_token(&[TokenType::Funk]) {
return Ok(Stmt::Function(self.function("function")?));
}
if self.match_token(&[TokenType::Class]) {
return self.class_declaration();
}
self.statement()
}
/// Parse a statement
fn statement(&mut self) -> Result<Stmt, Error> {
if self.match_token(&[TokenType::For]) {
return self.for_statement();
}
if self.match_token(&[TokenType::If]) {
return self.if_statement();
}
if self.match_token(&[TokenType::Print]) {
return self.print_statement();
}
if self.match_token(&[TokenType::Return]) {
return self.return_statement();
}
if self.match_token(&[TokenType::While]) {
return self.while_statement();
}
if self.match_token(&[TokenType::LeftBrace]) {
return Ok(Stmt::Block(Box::new(Block { statements: self.block()? })));
}
self.expression_statement()
}
/// Parse a variable declaration
fn variable_declaration(&mut self) -> Result<Stmt, Error> {
let name = self.consume(TokenType::Identifier, "Expected variable name.")?;
let initializer =
if self.match_token(&[TokenType::Equal]) { Some(self.expression()?) } else { None };
self.check_statement_end()?;
Ok(Stmt::Variable(statements::Variable { name, initializer }))
}
/// Parse a function declaration
fn function(&mut self, kind: &str) -> Result<Function, Error> {
let name = self.consume(TokenType::Identifier, &format!("Expected {} name.", kind))?;
self.consume(TokenType::LeftParen, &format!("Expected '(' after {} name.", kind))?;
let mut parameters = Vec::new();
if !self.check(&TokenType::RightParen) {
loop {
if parameters.len() > 255 {
return Err(Error::ParseError(
self.peek(),
"Cannot have more than 255 parameters.".to_string(),
));
}
parameters.push(self.consume(TokenType::Identifier, "Expected parameter name.")?);
if !self.match_token(&[TokenType::Comma]) {
break;
}
}
}
self.consume(TokenType::RightParen, "Expected ')' after parameters.")?;
self.consume(TokenType::LeftBrace, &format!("Expected '{{' before {} body.", kind))?;
let body = self.block()?;
Ok(statements::Function { name, params: parameters, body })
}
fn class_declaration(&mut self) -> Result<Stmt, Error> {
let name = self.consume(TokenType::Identifier, "Expected class name.")?;
self.consume(TokenType::LeftBrace, "Expected '{' before class body.")?;
let mut methods = Vec::new();
while !self.check(&TokenType::RightBrace) && !self.is_at_end() {
methods.push(self.function("method")?);
}
self.consume(TokenType::RightBrace, "Expected '}' after class body.")?;
Ok(Stmt::Class(Class { name, methods }))
}
/// Parse an if statement
fn if_statement(&mut self) -> Result<Stmt, Error> {
self.consume(TokenType::LeftParen, "Expected '(' after 'if'.")?;
let condition = self.expression()?;
self.consume(TokenType::RightParen, "Expected ')' after if condition.")?;
let then_branch = self.statement()?;
let else_branch =
if self.match_token(&[TokenType::Else]) { Some(self.statement()?) } else { None };
Ok(Stmt::If(Box::new(If { condition, then_branch, else_branch })))
}
/// Parse a for statement. We essentially craft a while loop with a declaration and an iterator
/// This is called "desugaring"
fn for_statement(&mut self) -> Result<Stmt, Error> {
self.consume(TokenType::LeftParen, "Expected '(' after 'for'.")?;
// Check for initializer, if it has been omitted, we will set it to None
let initializer: Option<Stmt> = if self.match_token(&[TokenType::Semicolon]) {
None
} else if self.match_token(&[TokenType::Var]) {
Some(self.variable_declaration()?)
} else {
Some(self.expression_statement()?)
};
// Check for condition, if it has been omitted, we will set it to None
let condition = if !self.check(&TokenType::Semicolon) {
self.expression()?
} else {
Expr::Literal(Literal { value: LiteralType::Bool(true) })
};
self.consume(TokenType::Semicolon, "Expected ';' after loop condition.")?;
// Check for increment, if it has been omitted, we will set it to None
let increment =
if !self.check(&TokenType::RightParen) { Some(self.expression()?) } else { None };
self.consume(TokenType::RightParen, "Expected ')' after for clauses.")?;
// Parse the body of the for loop
let mut body = self.statement()?;
// If there is an increment, we will add it to the end of the body
if let Some(increment) = increment {
body = Stmt::Block(Box::new(Block {
statements: vec![body, Stmt::Expression(Expression { expression: increment })],
}));
}
// Add in the while condition
body = Stmt::While(Box::new(While { condition, body }));
// If there is an initializer, we will add it to the beginning of the body
if let Some(initializer) = initializer {
body = Stmt::Block(Box::new(Block { statements: vec![initializer, body] }));
}
Ok(body)
}
/// Parse a print statement
fn print_statement(&mut self) -> Result<Stmt, Error> {
let expression = self.expression()?;
self.check_statement_end()?;
Ok(Stmt::Print(Print { expression }))
}
fn return_statement(&mut self) -> Result<Stmt, Error> {
let keyword = self.previous();
let value = match self.check(&TokenType::Semicolon) {
true => None,
false => Some(self.expression()?),
};
self.check_statement_end()?;
Ok(Stmt::Return(Return { keyword, value }))
}
/// Parse a while statement
fn while_statement(&mut self) -> Result<Stmt, Error> {
self.consume(TokenType::LeftParen, "Expected '(' after 'while'.")?;
let condition = self.expression()?;
self.consume(TokenType::RightParen, "Expected ')' after while condition.")?;
let body = self.statement()?;
Ok(Stmt::While(Box::new(While { condition, body })))
}
/// Return a list of statements between curly braces.
/// Note, this returns a Vec<Stmt> instead of a Block as we will reuse this code for
/// function bodies
fn block(&mut self) -> Result<Vec<Stmt>, Error> {
let mut statements = Vec::new();
while !self.check(&TokenType::RightBrace) && !self.is_at_end() {
statements.push(self.declaration()?);
}
self.consume(TokenType::RightBrace, "Expected '}' after block.")?;
Ok(statements)
}
/// Parse an expression statement
fn expression_statement(&mut self) -> Result<Stmt, Error> {
let expression = self.expression()?;
self.check_statement_end()?;
Ok(Stmt::Expression(Expression { expression }))
}
/// Check whether we are at the end of a statement. We accept both semi-colons and new lines
fn check_statement_end(&mut self) -> Result<(), Error> {
// Semi colon always ends a statement
if self.check(&TokenType::Semicolon) {
self.advance();
return Ok(());
}
// If we are at a new line, we can end the statement
let token: Token = self.peek();
if self.previous().line < token.line {
return Ok(());
}
Err(Error::ParseError(token, "Expected ';' or new line after expression.".to_string()))
}
/// ================================ EXPRESSIONS
/// Parse an expression
fn expression(&mut self) -> Result<Expr, Error> {
self.assignment()
}
fn assignment(&mut self) -> Result<Expr, Error> {
let expr = self.or()?;
if self.match_token(&[
TokenType::Equal,
TokenType::PlusEqual,
TokenType::MinusEqual,
TokenType::StarEqual,
TokenType::SlashEqual,
TokenType::MinusMinus,
TokenType::PlusPlus,
]) {
let operator = self.previous();
let value = if operator.token_type == TokenType::Equal {
self.or()?
} else if operator.token_type == TokenType::MinusMinus ||
operator.token_type == TokenType::PlusPlus
{
// Value is ++ or --, so we will operate on 1
let right = Expr::Literal(Literal { value: LiteralType::Number(1.0) });
Expr::Binary(Box::new(Binary {
left: expr.clone(),
operator: operator.clone(),
right,
}))
} else {
// Value is an operation as well as an assignment (+= 1 etc)
let right = self.or()?;
Expr::Binary(Box::new(Binary {
left: expr.clone(),
operator: operator.clone(),
right,
}))
};
// Check if we are assigning a variable or a property on an instance
if let Expr::Variable(variable) = expr.clone() {
return Ok(Expr::Assign(Box::new(Assign { name: variable.name, value })));
} else if let Expr::Get(assign) = &expr {
return Ok(Expr::Set(Box::new(Set {
object: assign.object.clone(),
name: assign.name.clone(),
value,
})));
} else if let Expr::Index(index) = &expr {
return Ok(Expr::Set(Box::new(Set {
object: Expr::Index(index.clone()),
name: Token {
token_type: TokenType::String,
lexeme: "LEXEME".to_string(),
literal: LiteralType::String("Idk".to_string()),
line: operator.line,
},
value,
})));
return Ok(Expr::Set(Box::new(Set {
object: index.object.clone(),
name: index.index.clone(),
value,
})));
}
return Err(Error::ParseError(operator, "Invalid assignment target.".to_string()));
}
Ok(expr)
}
fn or(&mut self) -> Result<Expr, Error> {
let mut expr = self.and()?;
while self.match_token(&[TokenType::Or]) {
let operator = self.previous();
let right = self.and()?;
expr = Expr::Logical(Box::new(Logical { left: expr, operator, right }));
}
Ok(expr)
}
fn and(&mut self) -> Result<Expr, Error> {
let mut expr = self.equality()?;
while self.match_token(&[TokenType::And]) {
let operator = self.previous();
let right = self.equality()?;
expr = Expr::Logical(Box::new(Logical { left: expr, operator, right }));
}
Ok(expr)
}
/// Not equal and equal
fn equality(&mut self) -> Result<Expr, Error> {
let mut expr = self.comparison()?;
while self.match_token(&[TokenType::BangEqual, TokenType::EqualEqual]) {
let operator = self.previous();
let right = self.comparison()?;
expr = Expr::Binary(Box::new(Binary { left: expr, operator, right }));
}
Ok(expr)
}
/// Greater than, greater than or equal, less than, less than or equal
fn comparison(&mut self) -> Result<Expr, Error> {
let mut expr = self.term()?;
while self.match_token(&[
TokenType::Greater,
TokenType::GreaterEqual,
TokenType::Less,
TokenType::LessEqual,
]) {
let operator = self.previous();
let right = self.term()?;
expr = Expr::Binary(Box::new(Binary { left: expr, operator, right }));
}
Ok(expr)
}
/// Addition and subtraction
fn term(&mut self) -> Result<Expr, Error> {
let mut expr = self.factor()?;
while self.match_token(&[TokenType::Minus, TokenType::Plus]) {
let operator = self.previous();
let right = self.factor()?;
expr = Expr::Binary(Box::new(Binary { left: expr, operator, right }));
}
Ok(expr)
}
/// Multiplication and division
fn factor(&mut self) -> Result<Expr, Error> {
let mut expr = self.modulo()?;
while self.match_token(&[TokenType::Slash, TokenType::Star]) {
let operator = self.previous();
let right = self.unary()?;
expr = Expr::Binary(Box::new(Binary { left: expr, operator, right }));
}
Ok(expr)
}
fn modulo(&mut self) -> Result<Expr, Error> {
let mut expr = self.unary()?;
while self.match_token(&[TokenType::Modulo]) {
let operator = self.previous();
let right = self.unary()?;
expr = Expr::Binary(Box::new(Binary { left: expr, operator, right }));
}
Ok(expr)
}
/// Unary negation
fn unary(&mut self) -> Result<Expr, Error> {
if self.match_token(&[TokenType::Bang, TokenType::Minus]) {
let operator = self.previous();
let right = self.unary()?;
return Ok(Expr::Unary(Box::new(Unary { operator, right })));
}
self.call()
}
fn call(&mut self) -> Result<Expr, Error> {
let mut expr = self.index_array()?;
loop {
if self.match_token(&[TokenType::LeftParen]) {
expr = self.finish_call(expr)?;
} else if self.match_token(&[TokenType::Dot]) {
let name =
self.consume(TokenType::Identifier, "Expected property name after '.'.")?;
expr = Expr::Get(Box::new(Get { object: expr, name }));
} else {
break;
}
}
Ok(expr)
}
fn finish_call(&mut self, callee: Expr) -> Result<Expr, Error> {
let mut arguments = Vec::with_capacity(255);
if !self.check(&TokenType::RightParen) {
loop {
if arguments.len() > 255 {
return Err(Error::ParseError(
self.peek(),
"Cannot have more than 255 arguments.".to_string(),
));
}
arguments.push(self.expression()?);
if !self.match_token(&[TokenType::Comma]) {
break;
}
}
}
let paren = self.consume(TokenType::RightParen, "Expected ')' after call arguments.")?;
Ok(Expr::Call(Box::new(Call { callee, paren, arguments })))
}
fn index_array(&mut self) -> Result<Expr, Error> {
let mut expr = self.primary()?;
while self.match_token(&[TokenType::LeftSquare]) {
let index = self.expression()?;
self.consume(TokenType::RightSquare, "Expected ']' after index.")?;
expr = Expr::Index(Box::new(Index { object: expr, index }));
}
Ok(expr)
}
/// Primary expression
fn primary(&mut self) -> Result<Expr, Error> {
if self.match_token(&[TokenType::LeftSquare]) {
return self.array();
}
if self.match_token(&[TokenType::False]) {
return Ok(Expr::Literal(Literal { value: LiteralType::Bool(false) }));
}
if self.match_token(&[TokenType::True]) {
return Ok(Expr::Literal(Literal { value: LiteralType::Bool(true) }));
}
if self.match_token(&[TokenType::Null]) {
return Ok(Expr::Literal(Literal { value: LiteralType::Null }));
}
if self.match_token(&[TokenType::Number, TokenType::String]) {
return Ok(Expr::Literal(Literal { value: self.previous().literal }));
}
if self.match_token(&[TokenType::Identifier]) {
return Ok(Expr::Variable(expressions::Variable { name: self.previous() }));
}
if self.match_token(&[TokenType::LeftParen]) {
let expr = self.expression()?;
self.consume(TokenType::RightParen, "Expect ')' after expression.")?;
return Ok(Expr::Grouping(Box::new(Grouping { expression: expr })));
}
let token = self.peek();
Err(Error::ParseError(token, "Expected expression.".to_string()))
}
fn array(&mut self) -> Result<Expr, Error> {
let mut elements = Vec::new();
if !self.check(&TokenType::RightSquare) {
loop {
elements.push(self.expression()?);
if !self.match_token(&[TokenType::Comma]) {
break;
}
}
}
self.consume(TokenType::RightSquare, "Expected ']' after array elements.")?;
Ok(Expr::Array(Box::new(Array { values: elements })))
}
/// Since we have thrown an error, we need to synchronize the parser to the next
/// statement boundary. We will catch the exception there and continue parsing
fn synchronize(&mut self) {
self.advance();
while !self.is_at_end() {
if self.previous().token_type == TokenType::Semicolon {
return;
}
// Advance until we meet a statement boundary
match self.peek().token_type {
TokenType::Class |
TokenType::Funk |
TokenType::Var |
TokenType::For |
TokenType::If |
TokenType::While |
TokenType::Print |
TokenType::Return => return,
_ => {
let _ = self.advance();
},
};
}
}
/// Check to see if the current token has any of the given types
fn match_token(&mut self, tokens: &[TokenType]) -> bool {
for token in tokens {
if self.check(token) {
self.advance();
return true;
}
}
false
}
/// returns true if the current token is of the given type. It never consumes the token,
/// only looks at it
fn check(&self, token_type: &TokenType) -> bool {
if self.is_at_end() {
return false;
}
return &self.peek().token_type == token_type;
}
/// Advance the current token and return the previous token
fn advance(&mut self) -> Token {
if !self.is_at_end() {
self.current += 1;
}
self.previous()
}
/// Consume a token if it is the expected token, if not we throw an error
fn consume(&mut self, token_type: TokenType, message: &str) -> Result<Token, Error> {
if self.check(&token_type) {
return Ok(self.advance());
}
return Err(Error::ParseError(self.peek(), message.to_string()));
}
/// Are we at the end of the list of tokens
fn is_at_end(&self) -> bool {
self.peek().token_type == TokenType::Eof
}
/// Get the next token we haven't consumed
fn peek(&self) -> Token {
self.tokens[self.current].clone()
}
/// Get the most recently consumed token
fn previous(&self) -> Token {
self.tokens[self.current - 1].clone()
}
}