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

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

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

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

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 Project

GAME DEV

Below are some of my more recent Game Development projects, all developed solo in Unity

Kiwi Ingenuity

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

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 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

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


Gallery image 1
Gallery image 2
Gallery image 3
Gallery image 4
Gallery image 5
Gallery image 6
Gallery image 7
Gallery image 8
Gallery image 9
Gallery image 10
Gallery image 11
Gallery image 12
Gallery image 13
Gallery image 14
Gallery image 15
Gallery image 16
Gallery image 17
Gallery image 18
Gallery image 19
Gallery image 20
Gallery image 21
Gallery image 22
Gallery image 23
Gallery image 24
Gallery image 25
Gallery image 26
Gallery image 27
Gallery image 28
Gallery image 29
Gallery image 30
Gallery image 31
Gallery image 32
Gallery image 33
Gallery image 34
Gallery image 35
Gallery image 36
Gallery image 37
Gallery image 38
Gallery image 39
Gallery image 40
Gallery image 41
Gallery image 42
Gallery image 43
Gallery image 44
Gallery image 45
Gallery image 46
Gallery image 47
Gallery image 48
Gallery image 49
Gallery image 50
Gallery image 51
Gallery image 52
Gallery image 53
Gallery image 54
Gallery image 55
Gallery image 56
Gallery image 57
Gallery image 58
Gallery image 59
Gallery image 60
Gallery image 61
Gallery image 62
Gallery image 63
Gallery image 64
Gallery image 65
Gallery image 66
Gallery image 67
Gallery image 68
Gallery image 69
Gallery image 70
Gallery image 71
Gallery image 72
Gallery image 73
Gallery image 74

Pokemon City

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()
	}
}