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
The following algorithms are used by the AI BOT in this project - for a detailed discussion see Road to JDK 25 - An Algorithmic Interlude:
- Random See: Random.java
- Minimax See: Minimax.java
- Minimax w. Alpha-Beta See: AlphaBeta.java
- MaxN See: MaxN.java
- Paranoid See: Paranoid.java
- Monte Carlo Tree Search See MonteCarloTreeSearch.java
https://openjdk.org/projects/jdk/25/
- JEP512: Compact Source Files and Instance Main Methods
- See: AppLite.java — compact source file with a top-level
main. - How to run: run_lite.sh
- See: AppLite.java — compact source file with a top-level
- JEP511: Module Import Declarations
- See:
AppLiteHttp.javausesimport module java.net.http;to simplify imports while using the JDK HTTP client.- File: AppLiteHttp.java
- How to run: run_lite_http.sh
- 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());
- See:
- JEP513: Flexible Constructor Bodies
- Allows statements before
super(...). Example: GameServiceException.java validatescausebeforesuper(message, cause).
- Allows statements before
- JEP506: Scoped Values (See: Game.java, GameTest.java)
- Uses
ScopedValueto bind a per-playGameContext(see: GameContext.java) withidandcreatedAt, scoped strictly toplay()execution; verified in tests.
- Uses
- JEP514: Ahead-of-Time Command-Line Ergonomics (See: 2a_aot_record_create_one_step.sh)
- One-step AOT workflow using
-XX:AOTCacheOutput=app.aotto record and create in a single invocation. Production runs use-XX:AOTCache=app.aot(see: 3a_aot_run.sh). - Optional: Set
JDK_AOT_VM_OPTIONSto pass options that apply only during the cache creation phase.
- One-step AOT workflow using
- JEP510: Key Derivation Function API (HKDF)
- Use HKDF-SHA256 (
javax.crypto.KDF) to derive AES-GCM keys from the Kyber KEM secret (no raw slicing). - See: SecureDuplexMessageHandler.java
- Use HKDF-SHA256 (
https://openjdk.org/projects/jdk/24/
- JEP496: Quantum-Resistant Module-Lattice-Based Key Encapsulation Mechanism (See: KyberKEMSpi.java, SecureConnectionTest.java)
- Implements ML-KEM via JCE SPI and uses BouncyCastle PQC; tests establish secure client/server handshake.
- JEP485: Stream Gatherers (See: Analyzers.java, StrategicTurningPoint.java)
- Custom
Gathererdiscovers strategic turning points while streamingGameStatehistory.
- Custom
- JEP483: Ahead-of-Time Class Loading & Linking (See: app/scripts/, e.g., 1_aot_record.sh, 2_aot_create.sh, 3a_aot_run.sh)
- Scripts record, generate, and run with AOT class data to improve startup.
- JEP484: Class-File API (See: NaiveFifoGenerator.java)
- Uses the
java.lang.classfileAPI to generate a simple bot class at runtime.
- Uses the
https://openjdk.org/projects/jdk/23/
- JEP467: Markdown Documentation Comments (See: Player.java)
- Uses Markdown-friendly documentation with
@snippetto embed example usage.
- Uses Markdown-friendly documentation with
- JEP474: ZGC: Generational Mode by Default
- Example: Configure ZGC via JVM args in Gradle
- App: app/build.gradle.kts —
applicationDefaultJvmArgs = listOf("--enable-native-access=ALL-UNNAMED", "-XX:+UseZGC") - TCP Game Server tests: tcp-gameserver/build.gradle.kts —
jvmArgs = listOf("--enable-native-access=ALL-UNNAMED", "-XX:+UseZGC")
- App: app/build.gradle.kts —
- Example: Configure ZGC via JVM args in Gradle
- JEP471: Deprecate the Memory-Access Methods in sun.misc.Unsafe for Removal
- Not used; examples with standard APIs: FFM (TicTacToeGameBoard.java, TicTacToeLibrary.java), Cleaner (TicTacToeGameBoard.java), VarHandles (PlayerIds.java).
https://openjdk.org/projects/jdk/22/
- JEP454: Foreign Function & Memory API (See: TicTacToeLibrary.java, TicTacToeGameBoard.java)
- Calls native functions via
Linker/SymbolLookup, usesMemorySegmentand upcall stubs.
- Calls native functions via
- JEP456: Unnamed Variables & Patterns (See: PlayerPrinter.java, GameClient.java)
- Demonstrates unnamed catch variables: e.g.,
catch (UnknownHostException _)andcatch (ConnectException _).
- Demonstrates unnamed catch variables: e.g.,
https://openjdk.org/projects/jdk/21/
- JEP431: Sequenced Collections (See:
history()in Game.java)- Uses
SequencedCollection<GameState>for ordered game history.
- Uses
- 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
switchoverGameContext.
- Demonstrates record pattern deconstruction and guarded cases in a
- JEP441: Pattern Matching for switch (See:
handleException(...)in GameServer.java)- Uses a
switchwith type patterns to handle root causes (e.g.,SocketTimeoutException).
- Uses a
- JEP444: Virtual Threads (See: GameServer.java)
- Handles many concurrent games using
Thread.ofVirtual()andExecutors.newThreadPerTaskExecutor.
- Handles many concurrent games using
- 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
jwebservervia helper script to serve static files locally; see Quick Start below.
- Uses JDK's built-in
- JEP413: Code Snippets in Java API Documentation (See: Player.java)
- Uses
@snippetto embed example usage directly in Javadoc.
- Uses
- JEP421: Deprecate Finalization for Removal (See: TicTacToeGameBoard.java)
- Avoids finalization in favor of
Cleanerfor native resource management.
- Avoids finalization in favor of
https://openjdk.org/projects/jdk/17/
- JEP421: Deprecate Finalization for Removal (See: TicTacToeGameBoard.java)
- Uses
Cleanerover finalization for native resource management.
- Uses
- JEP409: Sealed Classes (See: StrategicTurningPoint.java)
StrategicTurningPointis 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.kts —
org.graalvm.buildtools.native)- Example tasks:
./gradlew :app:nativeCompile,./gradlew :app:nativeRun
- Example tasks:
- Alternative: Use GraalVM Native Image via Gradle (See: app/build.gradle.kts —
- JEP415: Context-Specific Deserialization Filters (See: GamePersistence.java)
- Applies an
ObjectInputFilterbefore deserialization and uses guarded checks.
- Applies an
-
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- Requires JDK with GraalVM Native Image tooling. This project is configured with the Gradle plugin
org.graalvm.buildtools.nativeinapp/build.gradle.kts. - Build and run the native image:
./gradlew :app:nativeCompile
./gradlew :app:nativeRun-
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'- Serve the project directory (e.g., to host
index.html) using JDK’s built-injwebservervia the helper scriptserve.sh(wrapsjwebserver). Usage:serve.sh <port> <directory>:
# Serve current directory on port 8000
app/scripts/serve.sh 8000 .
# then open http://localhost:8000- Over-Engineering Tic-Tac-Toe - CLI Run overengineered tic-tac-toe from the command line
- Over-Engineering Tic-Tac-Toe - Microservices Microservice-based version of the project
