SPACEPORT DOCS

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:

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.

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: