Your First Application: Tic-Tac-Toe
Welcome to Spaceport! This tutorial is designed to be your first hands-on experience, guiding you through the creation of a simple but fully functional Tic-Tac-Toe game. It's the perfect way to see Spaceport's core features in action without the commitment of a full scaffold.
By the end of this guide, you will have learned the basics of:
- Minimal Scaffolding: Setting up a project with just a couple of folders.
- Source Modules: Writing your application's logic in a Groovy file.
- State Management: Storing the game's state (the board, current player) on the server.
- Launchpad Templates: Creating the user interface with
.ghtmlfiles. - Server Actions & Transmissions: Making the UI interactive by connecting button clicks directly to your server-side Groovy code.
We'll assume you have already completed the Developer Onboarding Guide and have Java and CouchDB installed and running.
If you're curious what this project looks like when it's finished, check out the Tic-Tac-Toe Live Demo. Note: This demo is slightly enhanced to allow for multiple simultaneous games bound to the client's docking session, an enhancement that is part of the "Taking It Further" section at the end of this tutorial.
# Step 1: Create the Project Structure
First, let's create a minimal project scaffold. All you need is a main folder for your project and a couple of subdirectories.
- Create a new folder for your project (e.g.,
my-tictactoe-app). - Inside that folder, create two subdirectories:
modulesandlaunchpad. - Inside
launchpad, create one more subdirectory namedparts.
Your scaffolding structure should look like this:
+ /my-tictactoe-app
+ modules/
+ launchpad/
+ parts/
+ elements/
Next, download the latest Spaceport JAR file into your main project folder.
curl -L https://spaceport.com.co/builds/spaceport-latest.jar -o spaceport.jar
Your near-complete project scaffold should now look like this:
+ /my-tictactoe-app
- spaceport.jar
+ modules/
+ launchpad/
+ parts/
+ elements/
# Step 2: Write the Game Logic & Router
All of our server-side logic will live in a single Source Module. This file will manage the game's state, handle player moves, and tell Spaceport how to serve our game to the browser.
Create a new file named Game.groovy inside your modules/ folder and let's build it piece by piece.
## Part A: Game State
First, define the class and the variables that will hold our game's state. We use static variables so they are shared
across all requests and act as a simple, in-memory "database" for our game.
import spaceport.computer.alerts.Alert
import spaceport.computer.alerts.results.*
import spaceport.launchpad.Launchpad
class Game {
//
// Game State
static List<String> board // A list of 9 strings representing the board ('X', 'O', or '')
static String currentPlayer // Who's turn it is, 'X' or 'O'
static String winner // Who won, 'X', 'O', 'Draw', or null if the game is ongoing
// ... more code will go here
}
## Part B: Core Game Logic
Next, add the methods that control the game. These methods will reset the board, process a player's move, and check for a winner.
// ... inside the Game class
//
// Core Game Logic
// Sets the game back to its starting state
static void resetGame() {
board = (0..8).collect { '' } // A list of 9 empty strings
currentPlayer = 'X'
winner = null
}
// Handles a player's move
static void makeMove(int index) {
// Only allow moves if the game isn't over and the square is empty
if (winner == null && board[index] == '') {
// Place the current player's mark on the board
board[index] = currentPlayer
checkWinner() // Check if this move won the game
// Switch player if the game is still going
if (winner == null) {
currentPlayer = (currentPlayer == 'X' ? 'O' : 'X')
}
}
}
// Checks if a player has won or if the game is a draw and updates
// the 'winner' variable (game state) accordingly
static void checkWinner() {
def winningLines = [
[0, 1, 2], [3, 4, 5], [6, 7, 8], // Rows
[0, 3, 6], [1, 4, 7], [2, 5, 8], // Columns
[0, 4, 8], [2, 4, 6] // Diagonals
]
// Check all winning combinations to see if any match the board
for (line in winningLines) {
def a = line[0]; def b = line[1]; def c = line[2]
if (board[a] && board[a] == board[b] && board[a] == board[c]) {
// We have a winner!
winner = board[a]
return
}
}
// Or, check for a draw (no empty squares left)
if (!board.contains('')) winner = 'Draw'
// No winner yet, game continues
}
## Part C: Spaceport Hooks & Router
Finally, we need to connect our logic to Spaceport. We'll use Alerts to hook into Spaceport's event system. One alert will initialize the game when the server starts, and another will act as a router to show the game in the browser.
// ... inside the Game class
//
// Spaceport Alerts (Hooks) & Router
static def launchpad = new Launchpad() // Create a Launchpad instance
// This Alert runs once when the application starts.
@Alert('on initialize')
static _init(Result r) {
resetGame() // Initialize the game board
}
// This Alert tells Spaceport what to do when someone visits our website's root URL ("/").
@Alert('on / hit')
static _renderGame(HttpResult r) {
// This command tells Launchpad to render the 'index.ghtml' template with the context from 'r'.
launchpad.assemble(['index.ghtml']).launch(r)
}
## Completed Game.groovy File
When you're done, your modules/Game.groovy file should look like this:
import spaceport.computer.alerts.Alert
import spaceport.computer.alerts.results.*
import spaceport.launchpad.Launchpad
class Game {
//
// Game State
static List<String> board
static String currentPlayer
static String winner
//
// Core Game Logic
static void resetGame() {
board = (0..8).collect { '' }
currentPlayer = 'X'
winner = null
}
static void makeMove(int index) {
if (winner == null && board[index] == '') {
board[index] = currentPlayer
checkWinner()
if (winner == null) {
currentPlayer = (currentPlayer == 'X' ? 'O' : 'X')
}
}
}
static void checkWinner() {
def winningLines = [
[0, 1, 2], [3, 4, 5], [6, 7, 8],
[0, 3, 6], [1, 4, 7], [2, 5, 8],
[0, 4, 8], [2, 4, 6]
]
for (line in winningLines) {
def a = line[0]; def b = line[1]; def c = line[2]
if (board[a] && board[a] == board[b] && board[a] == board[c]) {
winner = board[a]
return
}
}
if (!board.contains('')) winner = 'Draw'
}
//
// Spaceport Hooks & Router
static def launchpad = new Launchpad()
@Alert('on initialize')
static _init(Result r) {
resetGame()
}
@Alert('on / hit')
static _renderGame(HttpResult r) {
launchpad.assemble(['index.ghtml']).launch(r)
}
}
With the newly created Source Module, your project scaffold looks like this:
+ /my-tictactoe-app
- spaceport.jar
+ modules/
- Game.groovy
+ launchpad/
+ parts/
+ elements/
# Step 3: Create the User Interface
Now, let's create the visual part of our game using a Launchpad template. This .ghtml file will
contain our HTML, CSS, and some special Spaceport attributes to make the game interactive.
Create a new file named index.ghtml inside your launchpad/parts/ folder.
## Part A: HTML Boilerplate and Styling
Start with the basic HTML structure. We need to include the HUD-Core.js script, which is essential for enabling
Launchpad's interactive features like Server Actions and Transmissions. For simplicity, we'll embed our CSS directly
in a <style> tag.
Launchpad has the idea of a Vessel, which can act like a wrapper for your entire page, letting you abstract some of the boilerplate away. However, for this simple example, we'll just use a single file.
<!DOCTYPE html>
<html>
<head>
<title>Spaceport Tic-Tac-Toe</title>
<script defer src='https://cdn.jsdelivr.net/gh/spaceport-dev/hud-core.js@latest/hud-core.min.js'></script>
<style>
@view-transition { navigation: auto; }
body { user-select: none; font-family: sans-serif; display: grid; place-content: center; text-align: center; background-color: black; color: white; gap: 40px; }
.board { display: grid; grid-template-columns: repeat(3, 100px); gap: 5px; background-color: white; border: 5px solid white; border-radius: 18px; }
.cell { aspect-ratio: 1/1; background-color: black; font-size: 3em; display: grid; place-content: center; cursor: pointer; border-radius: 10px;}
.cell:hover { outline: 5px solid dodgerblue; }
.status { background-color: white; color: black; border-radius: 10px; font-size: 1.5em; height: 50px; display: grid; place-content: center; }
button { background-color: black; font-size: 1em; padding: 10px 20px; cursor: pointer; color: white; border: 5px solid white; border-radius: 10px; }
button:hover { outline: 5px solid dodgerblue; }
</style>
</head>
<body>
<h1>Spaceport <br> Tic-Tac-Toe</h1>
/// ... we'll add more code here in the next section
</body>
</html>
Note: You'll notice /// (triple slashes) in the code above. Inside Launchpad Templates, these are used to denote server comments which can be used inside your server-side Groovy code blocks, HTML, CSS, and JavaScript. They are ignored by the server and do not appear in the rendered HTML.
## Part B: Displaying Dynamic Content
Inside the <body>, we'll add the UI. First, a status message that dynamically changes based on the game's state. We
can embed Groovy code directly in our HTML using <% ... %> scriptlets to access the static variables from
our Game.groovy class.
/// ... inside the <body> tag, after the <h1> tag
<div class="status">
/// Use Groovy code to display the current game status
<% if (Game.winner) { %>
/// Conditionally render different messages based on the winner
${ "It's a draw!".if { Game.winner == "Draw" }}
${ "Player ${ Game.winner } wins!".if { Game.winner != "Draw" }}
<% } else { %>
/// If there's no winner yet, show whose turn it is
Player ${ Game.currentPlayer }'s Turn
<% } %>
</div>
## Part C: Rendering an Interactive Board
Next, we'll render the game board by looping through our Game.board list. The interactive magic happens in the on-click
attribute. This is a Server Action—one of Launchpad's core features. When a user clicks a cell, the Groovy code inside ${ _{ ... } } is executed
on the server. It calls our makeMove() method and then returns a Transmission, [ '@redirect' : '/' ], which
tells the browser to reload the page and show the updated game state. This could also be a @reload, but we've included
a standard view transition for a neat fade effect between turns which requires a page navigation, which @redirect provides.
/// ...inside the <body> tag, after the status <div>
<div class="board">
/// Loop through the board state from our Game.groovy file
<% Game.board.eachWithIndex { cell, i -> %>
<div class="cell" on-click="${ _{
Game.makeMove(i)
return [ '@redirect' : '/' ]
}}">
${ cell }
</div>
<% } %>
</div>
## Part D: The Reset Button
Finally, add a reset button that uses the same Server Action pattern to call Game.resetGame() and reload the page.
/// ... inside the <body> tag, after the board <div>
<button on-click="${ _{
Game.resetGame()
return [ '@redirect' : '/' ]
}}">
Reset Game
</button>
## Completed index.ghtml File
Your final launchpad/parts/index.ghtml file should look like this:
<!DOCTYPE html>
<html>
<head>
<title>Spaceport Tic-Tac-Toe</title>
<script defer src='https://cdn.jsdelivr.net/gh/spaceport-dev/hud-core.js@latest/hud-core.min.js'></script>
<style>
@view-transition { navigation: auto; }
body { user-select: none; font-family: sans-serif; display: grid; place-content: center; text-align: center; background-color: black; color: white; gap: 40px; }
.board { display: grid; grid-template-columns: repeat(3, 100px); gap: 5px; background-color: white; border: 5px solid white; border-radius: 18px; }
.cell { aspect-ratio: 1/1; background-color: black; font-size: 3em; display: grid; place-content: center; cursor: pointer; border-radius: 10px;}
.cell:hover { outline: 5px solid dodgerblue; }
.status { background-color: white; color: black; border-radius: 10px; font-size: 1.5em; height: 50px; display: grid; place-content: center; }
button { background-color: black; font-size: 1em; padding: 10px 20px; cursor: pointer; color: white; border: 5px solid white; border-radius: 10px; }
button:hover { outline: 5px solid dodgerblue; }
</style>
</head>
<body>
<h1>Spaceport <br> Tic-Tac-Toe</h1>
<div class="status">
/// Use Groovy code to display the current game status
<% if (Game.winner) { %>
/// Conditionally render different messages based on the winner
${ "It's a draw!".if { Game.winner == "Draw" }}
${ "Player ${ Game.winner } wins!".if { Game.winner != "Draw" }}
<% } else { %>
/// If there's no winner yet, show whose turn it is
Player ${ Game.currentPlayer }'s Turn
<% } %>
</div>
<div class="board">
/// Loop through the board state from our Game.groovy file
<% Game.board.eachWithIndex { cell, i -> %>
<div class="cell" on-click="${ _{
Game.makeMove(i)
return [ '@redirect' : '/' ]
}}">
${ cell }
</div>
<% } %>
</div>
<button on-click="${ _{
Game.resetGame()
return [ '@redirect' : '/' ]
}}">
Reset Game
</button>
</body>
</html>
With all files in place, your complete tic-tac-toe project scaffold looks like this:
+ /my-tictactoe-app
- spaceport.jar
+ modules/
- Game.groovy
+ launchpad/
+ parts/
- index.ghtml
+ elements/
# Step 4: Launch and Play!
That's it! You've written all the code needed for a complete, interactive web application.
To run your game, open your terminal, navigate to your project's root folder (my-tictactoe-app), and run the
following command:
java -jar spaceport.jar --start --no-manifest
Spaceport will start up. Now, open your web browser and go to: http://localhost:10000
You should see your Tic-Tac-Toe board. Click on the squares to play a game against yourself (or with somebody next to you), and use the reset button to start a new game.
# Taking It Further
Congratulations on building your first Spaceport application! You've just created a complete, interactive experience using a classic Multi-Page Application (MPA) pattern. It's a bit ironic to call it "multi-page" when there's only one view, but the term describes the technique: the server generates a completely new HTML document in response to each user action, which is a powerful and simple way to manage server-driven state.
This is a great starting point, but the true philosophy of Spaceport is a hybrid approach. It's designed to blend the simplicity of an MPA with the fluid, app-like experience of a Single-Page Application (SPA). A SPA avoids full page reloads by dynamically rewriting parts of the existing page with new data from the server. While SPAs and MPAs are poles apart in terms of architecture, Spaceport makes it easy to start with an MPA and gradually add SPA-like features when needed.
When you enhance an MPA with features like Spaceport's Launchpad Transmissions, it starts to exhibit the best qualities of a SPA. You get a smooth user experience without the complexity of a full client-side framework.
## Evolve Your Application
Here are a few ways to take your game to the next level:
- Implement Real-Time Updates (SPA-like qualities) Instead of reloading the entire page, use a targeted Launchpad Transmission to update only the clicked cell and the status message. The server will respond with tiny instructions to manipulate the existing page, making the game feel instantaneous. This is the first and most important step in shifting from a pure MPA to a hybrid model. There's a lot to learn with Launchpad and Transmissions, but it's worth the effort.
- Externalize Your Styles Improve your project's organization by moving the CSS into a dedicated
.cssfile and serving it using Spaceport's static asset handling. This allows the browser to cache your styles, improving performance. Spaceport's Static Assets documentation will guide you through the setup. This is also a great time to implement a Spaceport Manifest Configuration file to manage your app's scaffold and settings.
- Support Multiple, Simultaneous Games Currently, there's only one game board for everyone on the server. A great next step would be to refactor the state management to support multiple, independent game sessions so different users can play at the same time without interfering with each other. Learning about Docking Sessions is a good place to start.
- Build a True Multiplayer Experience Allow two players to compete from different browsers. You can replace the
staticvariables in theGameclass with a sharedCargoobject from the Spaceport Store to synchronize the game state between them in real-time. Check out the Cargo Documentation for more details.
SPACEPORT DOCS