Skip to content

briancorbinxyz/overengineering-tictactoe

Repository files navigation

Over-Engineering TicTacToe

Over-Engineering Tic-Tac-Toe

Tic-Tac-Toe in Java deliberately over-engineered to apply features of Java introduced over time.

Developed to pair with the ongoing blog post: Road to JDK 25 - Over-Engineering Tic-Tac-Toe also serialized to Medium @ Road to JDK 25 - Over-Engineering Tic-Tac-Toe On Medium


Algorithms

The following algorithms are used by the AI BOT in this project - for a detailed discussion see Road to JDK 25 - An Algorithmic Interlude:


Features

https://openjdk.org/projects/jdk/25/

  • JEP512: Compact Source Files and Instance Main Methods
  • JEP511: Module Import Declarations
    • See: AppLiteHttp.java uses import module java.net.http; to simplify imports while using the JDK HTTP client.
    • Example (jshell):
      import module java.net.http;
      var client = HttpClient.newHttpClient();
      var res = client.send(HttpRequest.newBuilder(java.net.URI.create("https://example.com")).build(), HttpResponse.BodyHandlers.ofString());
      System.out.println(res.statusCode());
  • JEP513: Flexible Constructor Bodies
  • JEP506: Scoped Values (See: Game.java, GameTest.java)
    • Uses ScopedValue to bind a per-play GameContext (see: GameContext.java) with id and createdAt, scoped strictly to play() execution; verified in tests.
  • JEP514: Ahead-of-Time Command-Line Ergonomics (See: 2a_aot_record_create_one_step.sh)
    • One-step AOT workflow using -XX:AOTCacheOutput=app.aot to record and create in a single invocation. Production runs use -XX:AOTCache=app.aot (see: 3a_aot_run.sh).
    • Optional: Set JDK_AOT_VM_OPTIONS to pass options that apply only during the cache creation phase.
  • JEP510: Key Derivation Function API (HKDF)

https://openjdk.org/projects/jdk/24/

https://openjdk.org/projects/jdk/23/

https://openjdk.org/projects/jdk/22/

https://openjdk.org/projects/jdk/21/

  • JEP431: Sequenced Collections (See: history() in Game.java)
    • Uses SequencedCollection<GameState> for ordered game history.
  • JEP439: Generational ZGC
    • Runtime/flag-level feature; no code changes required here.
  • JEP440: Record Patterns (See: GameContextTest.java)
    • Demonstrates record pattern deconstruction and guarded cases in a switch over GameContext.
  • JEP441: Pattern Matching for switch (See: handleException(...) in GameServer.java)
    • Uses a switch with type patterns to handle root causes (e.g., SocketTimeoutException).
  • JEP444: Virtual Threads (See: GameServer.java)
    • Handles many concurrent games using Thread.ofVirtual() and Executors.newThreadPerTaskExecutor.
  • JEP452: Key Encapsulation Mechanism API (See: KyberKEMSpi.java)
    • Implements the JCE KEM SPI to integrate ML-KEM with the standard crypto API.

https://openjdk.org/projects/jdk/20/

  • No new features

https://openjdk.org/projects/jdk/19/

  • No new features

https://openjdk.org/projects/jdk/18/

  • JEP400: UTF-8 by Default
    • Source files and runtime default charset assume UTF-8; no special handling required.
  • JEP408: Simple Web Server (See: serve.sh)
    • Uses JDK's built-in jwebserver via helper script to serve static files locally; see Quick Start below.
  • JEP413: Code Snippets in Java API Documentation (See: Player.java)
    • Uses @snippet to embed example usage directly in Javadoc.
  • JEP421: Deprecate Finalization for Removal (See: TicTacToeGameBoard.java)
    • Avoids finalization in favor of Cleaner for native resource management.

https://openjdk.org/projects/jdk/17/

  • JEP421: Deprecate Finalization for Removal (See: TicTacToeGameBoard.java)
    • Uses Cleaner over finalization for native resource management.
  • JEP409: Sealed Classes (See: StrategicTurningPoint.java)
    • StrategicTurningPoint is a sealed interface with record implementations.
  • JEP410: Remove the Experimental AOT and JIT Compiler
    • Alternative: Use GraalVM Native Image via Gradle (See: app/build.gradle.ktsorg.graalvm.buildtools.native)
      • Example tasks: ./gradlew :app:nativeCompile, ./gradlew :app:nativeRun
  • JEP415: Context-Specific Deserialization Filters (See: GamePersistence.java)
    • Applies an ObjectInputFilter before deserialization and uses guarded checks.

Quick Start

  • To run the single game application, use the following command: ./gradlew run

  • To run the lite HTTP demo (JEP 511 + java.net.http), use: app/scripts/run_lite_http.sh

  • If you don't have Java installed on your system you can install it first with SDKMAN to build with a JDK 25 toolchain:

curl -s "https://get.sdkman.io" | bash

sdk install java 25-zulu

./gradlew run

Native Image (GraalVM)

  • Requires JDK with GraalVM Native Image tooling. This project is configured with the Gradle plugin org.graalvm.buildtools.native in app/build.gradle.kts.
  • Build and run the native image:
./gradlew :app:nativeCompile
./gradlew :app:nativeRun

AOT Quick Start (JDK24+)

  • Requires a JDK with AOT cache support. For the one-step workflow (JEP 514), use JDK 25+.

  • One-step record+create (JDK25+):

# Create AOT cache in a single invocation (produces app.aot)
app/scripts/2a_aot_record_create_one_step.sh

# Run with the generated AOT cache
app/scripts/3a_aot_run.sh
  • Two-step workflow (works on JDK24+):
# 1) Record training configuration
app/scripts/1_aot_record.sh

# 2) Create the AOT cache (produces app.aot)
app/scripts/2_aot_create.sh

# 3) Run with the AOT cache
app/scripts/3a_aot_run.sh
  • Optional (JEP 514): Pass flags only for the create phase by setting JDK_AOT_VM_OPTIONS:
export JDK_AOT_VM_OPTIONS="-Xms512m -Xmx512m"
app/scripts/2a_aot_record_create_one_step.sh
  • Benchmark (optional): Requires hyperfine (see project page: https://github.com/sharkdp/hyperfine). Ensure you've created the AOT cache first using the steps above (one-step or two-step), then run:
hyperfine -n standard './scripts/bench.sh' -n aot './scripts/bench.sh --aot'

Simple Web Server (JEP 408)

  • Serve the project directory (e.g., to host index.html) using JDK’s built-in jwebserver via the helper script serve.sh (wraps jwebserver). Usage: serve.sh <port> <directory>:
# Serve current directory on port 8000
app/scripts/serve.sh 8000 .
# then open http://localhost:8000

Related

Packages