Jac in a Flash#
This mini tutorial uses a single toy program to highlight the major pieces of the Jac language. We start with a small Python game and gradually evolve it into a fully object‑spatial Jac implementation. Each iteration introduces a new Jac feature while keeping the overall behaviour identical.
Step 0 – The Python version#
Our starting point is a regular Python program that implements a simple "guess the number" game. The player has several attempts to guess a randomly generated number.
Step 1 – A direct Jac translation#
guess_game1.jac
mirrors the Python code almost line for line. Classes are
declared with obj
and methods with def
. Statements end with a semicolon and
the parent initializer is invoked via super.init
. Program execution happens
inside a with entry { ... }
block, which replaces Python's
if __name__ == "__main__":
section. This step shows how familiar Python
concepts map directly to Jac syntax.
"""A Number Guessing Game"""
import random;
class Game {
def init(self: Game, attempts: int = 10) {
self.attempts = attempts;
self.won = False;
}
def play(self: Game) {
raise NotImplementedError("Subclasses must implement this method.") ;
}
}
class GuessTheNumberGame(Game) {
def init(self: GuessTheNumberGame, attempts: int = 10) {
super.init(attempts);
self.correct_number = random.randint(1, 10);
}
def play(self: GuessTheNumberGame) {
while self.attempts > 0 {
guess = input("Guess a number between 1 and 10: ");
if guess.isdigit() {
self.process_guess(int(guess));
if self.won {
break;
}
} else {
print("That's not a valid number! Try again.");
}
}
if not self.won {
print("Sorry, you didn't guess the number. Better luck next time!");
}
}
def process_guess(self: GuessTheNumberGame, guess: int) {
if guess > self.correct_number {
print("Too high!");
} elif guess < self.correct_number {
print("Too low!");
} else {
print("Congratulations! You guessed correctly.");
self.won = True;
return;
}
self.attempts -= 1;
print(f"You have {self.attempts} attempts left.");
}
}
with entry {
game = GuessTheNumberGame();
game.play();
}
Step 2 – Declaring fields with has
#
The second version moves attribute definitions into the class body using the
has
keyword. Fields may specify types and default values directly on the
declaration. Methods that take no parameters omit parentheses in their
signature, making the object definition concise.
"""A Number Guessing Game"""
import random;
obj Game {
has attempts: int, won: bool = False;
def play {
raise NotImplementedError("Subclasses must implement this method.");
}
}
obj GuessTheNumberGame (Game) {
has attempts: int = 10, correct_number: int = random.randint(1, 10);
def play {
while self.attempts > 0 {
guess = input("Guess a number between 1 and 10: ");
if guess.isdigit() {
self.process_guess(int(guess));
if self.won {
break;
}
} else {
print("That's not a valid number! Try again.");
}
}
if not self.won {
print("Sorry, you didn't guess the number. Better luck next time!");
}
}
def process_guess(guess: int) {
if guess > self.correct_number {
print("Too high!");
} elif guess < self.correct_number {
print("Too low!");
} else {
print("Congratulations! You guessed correctly.");
self.won = True;
return;
}
self.attempts -= 1;
print(f"You have {self.attempts} attempts left.");
}
}
# Run the game
with entry {
game = GuessTheNumberGame();
game.play();
}
Step 3 – Separating implementation with impl
#
The fourth version splits object declarations from their implementations using
impl
. The object lists method signatures (def init;
, override def play;
),
and the actual bodies are provided later in impl Class.method
blocks. This
separation keeps the interface clean and helps organise larger codebases.
"""A Number Guessing Game"""
import random;
obj Game {
has attempts: int, won: bool = False;
def play;
}
obj GuessTheNumberGame (Game) {
has correct_number: int = random.randint(1, 10);
def init;
override def play;
def process_guess(guess: int);
}
# Run the game
with entry {
game = GuessTheNumberGame();
game.play();
}
impl Game.play{
raise NotImplementedError("Subclasses must implement this method.") ;
}
impl GuessTheNumberGame.init{
self.attempts = 10;
self.correct_number = random.randint(1, 10);
}
impl GuessTheNumberGame.play{
while self.attempts > 0 {
guess = input("Guess a number between 1 and 10: ");
if guess.isdigit() {
self.process_guess(int(guess));
} else {
print("That's not a valid number! Try again.");
}
}
if not self.won {
print("Sorry, you didn't guess the number. Better luck next time!");
}
}
impl GuessTheNumberGame.process_guess(guess: int) {
if guess > self.correct_number {
print("Too high!");
self.attempts -= 1;
} elif guess < self.correct_number {
print("Too low!");
self.attempts -= 1;
} else {
print("Congratulations! You guessed correctly.");
self.won = True;
self.attempts = 0;
return;
}
print(f"You have {self.attempts} attempts left.");
}
Step 4 – Walking the graph#
Finally guess_game4.jac
re‑imagines the game using Jac's object‑spatial
architecture. A walker
visits a chain of turn
nodes created with ++>
edges. The walker moves with visit [-->]
and stops via disengage
when the
guess is correct. The game is launched by spawn
ing the walker at root
.
This example shows how conventional logic can become graph traversal.
"""A Number Guessing Game"""
import random;
walker GuessGame {
has correct_number: int = random.randint(1, 10);
can start_game with `root entry;
def process_guess(guess: int);
}
node turn {
can check with GuessGame entry;
}
# Run the game
with entry {
GuessGame() spawn root;
}
impl turn.check{
guess = input("Guess a number between 1 and 10: ");
if guess.isdigit() {
visitor.process_guess(int(guess));
} else {
print("That's not a valid number! Try again.");
}
visit [-->];
}
impl GuessGame.start_game{
end: `root | turn = here;
for i = 0 to i < 10 by i += 1 {
end ++> (end := turn());
}
visit [-->];
}
impl GuessGame.process_guess(guess: int) {
if guess > self.correct_number {
print("Too high!");
} elif guess < self.correct_number {
print("Too low!");
} else {
print("Congratulations! You guessed correctly.");
disengage;
}
}
Step 5 – Scale Agnostic Approach#
The fifth version demonstrates Jac's scale-agnostic design. The same code that runs locally can seamlessly scale to cloud deployment without modification. By running the command jac serve filename.jac
, the walkers become API endpoints that can be called via HTTP requests. This shows how Jac applications are inherently cloud-ready.
"""A Number Guessing Game"""
import random;
walker GuessGame {
has guess: int;
can start with `root entry;
can process_guess with turn entry;
}
node turn {
has correct_number: int = random.randint(1, 10);
}
# Will run when in CLI mode (not in cloud)
with entry:__main__ {
root spawn GuessGame(3);
root spawn GuessGame(4);
root spawn GuessGame(5);
root spawn GuessGame(6);
}
impl GuessGame.start {
if not [root --> (`?turn)] {
next = root ++> turn(random.randint(1, 10));
} else {
next = [root --> (`?turn)];
}
visit next;
}
impl GuessGame.process_guess {
if [-->] {
visit [-->];
} else {
if self.guess < here.correct_number {
print("Too high!");
here ++> turn(here.correct_number);
} elif self.guess > here.correct_number {
print("Too low!");
here ++> turn(here.correct_number);
} else {
print("Congratulations! You guessed correctly.");
disengage;
}
}
}
Step 6 – AI-Enhanced Gameplay with byLLM#
The final version integrates AI capabilities using byLLM (Meaning Typed LLM). Instead of simple "too high" or "too low" responses, the game now provides intelligent, context-aware hints generated by an LLM. This demonstrates how easily AI can be woven into Jac applications to create more engaging user experiences.
Happy code deconstructing!