Added http4s server according to the API specification. A game is playable via API
This commit is contained in:
@@ -0,0 +1,79 @@
|
||||
import org.gradle.jvm.tasks.Jar
|
||||
|
||||
plugins {
|
||||
id("scala")
|
||||
id("org.scoverage") version "8.1"
|
||||
application
|
||||
}
|
||||
|
||||
group = "de.nowchess"
|
||||
version = "1.0-SNAPSHOT"
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
val versions = rootProject.extra["VERSIONS"] as Map<String, String>
|
||||
|
||||
repositories {
|
||||
mavenCentral()
|
||||
}
|
||||
|
||||
scala {
|
||||
scalaVersion = versions["SCALA3"]!!
|
||||
}
|
||||
|
||||
scoverage {
|
||||
scoverageVersion.set(versions["SCOVERAGE"]!!)
|
||||
excludedPackages.set(listOf("de.nowchess.server"))
|
||||
}
|
||||
|
||||
application {
|
||||
mainClass.set("de.nowchess.server.ServerApp")
|
||||
}
|
||||
|
||||
tasks.withType<ScalaCompile> {
|
||||
scalaCompileOptions.additionalParameters = listOf("-encoding", "UTF-8")
|
||||
}
|
||||
|
||||
tasks.named<Jar>("jar") {
|
||||
duplicatesStrategy = org.gradle.api.file.DuplicatesStrategy.EXCLUDE
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation("org.scala-lang:scala3-compiler_3") {
|
||||
version { strictly(versions["SCALA3"]!!) }
|
||||
}
|
||||
implementation("org.scala-lang:scala3-library_3") {
|
||||
version { strictly(versions["SCALA3"]!!) }
|
||||
}
|
||||
|
||||
implementation(project(":modules:api"))
|
||||
implementation(project(":modules:core"))
|
||||
implementation(project(":modules:io"))
|
||||
implementation(project(":modules:rule"))
|
||||
|
||||
val http4s = versions["HTTP4S"]!!
|
||||
val circe = versions["CIRCE"]!!
|
||||
|
||||
implementation("org.http4s:http4s-ember-server_3:$http4s")
|
||||
implementation("org.http4s:http4s-dsl_3:$http4s")
|
||||
implementation("org.http4s:http4s-circe_3:$http4s")
|
||||
implementation("io.circe:circe-core_3:$circe")
|
||||
implementation("io.circe:circe-generic_3:$circe")
|
||||
implementation("io.circe:circe-parser_3:$circe")
|
||||
|
||||
testImplementation(platform("org.junit:junit-bom:${versions["JUNIT_BOM"]!!}"))
|
||||
testImplementation("org.junit.jupiter:junit-jupiter")
|
||||
testImplementation("org.scalatest:scalatest_3:${versions["SCALATEST"]!!}")
|
||||
testImplementation("co.helmethair:scalatest-junit-runner:${versions["SCALATEST_JUNIT"]!!}")
|
||||
|
||||
testRuntimeOnly("org.junit.platform:junit-platform-launcher")
|
||||
}
|
||||
|
||||
tasks.test {
|
||||
useJUnitPlatform {
|
||||
includeEngines("scalatest")
|
||||
testLogging {
|
||||
events("skipped", "failed")
|
||||
}
|
||||
}
|
||||
finalizedBy(tasks.reportScoverage)
|
||||
}
|
||||
@@ -0,0 +1,283 @@
|
||||
package de.nowchess.server
|
||||
|
||||
import cats.effect.IO
|
||||
import org.http4s.*
|
||||
import org.http4s.dsl.io.*
|
||||
import org.http4s.circe.*
|
||||
import org.http4s.circe.CirceEntityDecoder.given
|
||||
import io.circe.syntax.*
|
||||
import de.nowchess.api.board.{Color, Square}
|
||||
import de.nowchess.api.move.{Move, MoveType, PromotionPiece}
|
||||
import de.nowchess.api.player.{PlayerId, PlayerInfo}
|
||||
import de.nowchess.chess.engine.GameEngine
|
||||
import de.nowchess.chess.observer.InvalidMoveEvent
|
||||
import de.nowchess.io.fen.{FenExporter, FenParser}
|
||||
import de.nowchess.io.pgn.{PgnExporter, PgnParser}
|
||||
import JsonCodecs.given
|
||||
|
||||
class BoardRoutes(registry: GameRegistry):
|
||||
|
||||
def routes: HttpRoutes[IO] = HttpRoutes.of[IO] {
|
||||
case req @ POST -> Root / "api" / "board" / "game" / "import" / "fen" => importFen(req)
|
||||
case req @ POST -> Root / "api" / "board" / "game" / "import" / "pgn" => importPgn(req)
|
||||
case req @ POST -> Root / "api" / "board" / "game" => createGame(req)
|
||||
case GET -> Root / "api" / "board" / "game" / id => getGame(id)
|
||||
case GET -> Root / "api" / "board" / "game" / id / "stream" => streamGame(id)
|
||||
case POST -> Root / "api" / "board" / "game" / id / "move" / uci => makeMove(id, uci)
|
||||
case req @ GET -> Root / "api" / "board" / "game" / id / "moves" => getLegalMoves(req, id)
|
||||
case POST -> Root / "api" / "board" / "game" / id / "undo" => undoMove(id)
|
||||
case POST -> Root / "api" / "board" / "game" / id / "redo" => redoMove(id)
|
||||
case POST -> Root / "api" / "board" / "game" / id / "resign" => resign(id)
|
||||
case POST -> Root / "api" / "board" / "game" / id / "draw" / action => drawAction(id, action)
|
||||
case GET -> Root / "api" / "board" / "game" / id / "export" / "fen" => exportFen(id)
|
||||
case GET -> Root / "api" / "board" / "game" / id / "export" / "pgn" => exportPgn(id)
|
||||
}
|
||||
|
||||
// ─── Game lifecycle ────────────────────────────────────────────────────────
|
||||
|
||||
private def createGame(req: Request[IO]): IO[Response[IO]] =
|
||||
req.as[CreateGameRequest]
|
||||
.handleError(_ => CreateGameRequest())
|
||||
.flatMap { body =>
|
||||
val white = resolvePlayer(body.white, "white")
|
||||
val black = resolvePlayer(body.black, "black")
|
||||
registry.create(white, black).flatMap { id =>
|
||||
registry.get(id).flatMap {
|
||||
case None => notFound(id)
|
||||
case Some(entry) => Created(toGameFullDto(id, entry).asJson)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private def getGame(id: String): IO[Response[IO]] =
|
||||
registry.get(id).flatMap {
|
||||
case None => notFound(id)
|
||||
case Some(entry) => Ok(toGameFullDto(id, entry).asJson)
|
||||
}
|
||||
|
||||
private def streamGame(id: String): IO[Response[IO]] =
|
||||
registry.get(id).flatMap {
|
||||
case None => notFound(id)
|
||||
case Some(entry) => Ok(GameFullEvent("gameFull", toGameFullDto(id, entry)).asJson)
|
||||
}
|
||||
|
||||
private def resign(id: String): IO[Response[IO]] =
|
||||
registry.get(id).flatMap {
|
||||
case None => notFound(id)
|
||||
case Some(entry) if entry.isOver => badRequest("GAME_OVER", "Game is already over")
|
||||
case Some(entry) =>
|
||||
IO.blocking {
|
||||
val winner = entry.engine.context.turn.opposite
|
||||
entry.gameOver = Some(GameOverState.Resigned(winner, entry.engine.context))
|
||||
}.flatMap(_ => Ok(OkDto().asJson))
|
||||
}
|
||||
|
||||
// ─── Move-making ───────────────────────────────────────────────────────────
|
||||
|
||||
private def makeMove(id: String, uci: String): IO[Response[IO]] =
|
||||
registry.get(id).flatMap {
|
||||
case None => notFound(id)
|
||||
case Some(entry) if entry.isOver => badRequest("GAME_OVER", "Game is already over")
|
||||
case Some(entry) =>
|
||||
IO.blocking {
|
||||
entry.lastEvent = None
|
||||
applyUci(entry, uci)
|
||||
entry.lastEvent
|
||||
}.flatMap {
|
||||
case Some(InvalidMoveEvent(_, reason)) => badRequest("INVALID_MOVE", reason)
|
||||
case _ => Ok(toGameStateDto(entry).asJson)
|
||||
}
|
||||
}
|
||||
|
||||
private def getLegalMoves(req: Request[IO], id: String): IO[Response[IO]] =
|
||||
registry.get(id).flatMap {
|
||||
case None => notFound(id)
|
||||
case Some(entry) if entry.isOver => badRequest("GAME_OVER", "Game is already over")
|
||||
case Some(entry) =>
|
||||
val squareOpt = req.uri.query.params.get("square").flatMap(Square.fromAlgebraic)
|
||||
IO.blocking {
|
||||
val ctx = entry.engine.context
|
||||
val rs = entry.engine.ruleSet
|
||||
squareOpt.map(rs.legalMoves(ctx)).getOrElse(rs.allLegalMoves(ctx))
|
||||
}.flatMap { moves =>
|
||||
Ok(LegalMovesDto(moves.map(toLegalMoveDto)).asJson)
|
||||
}
|
||||
}
|
||||
|
||||
private def undoMove(id: String): IO[Response[IO]] =
|
||||
registry.get(id).flatMap {
|
||||
case None => notFound(id)
|
||||
case Some(entry) if !entry.engine.canUndo => badRequest("UNDO_UNAVAILABLE", "No moves to undo")
|
||||
case Some(entry) =>
|
||||
IO.blocking { entry.engine.undo() }.flatMap(_ => Ok(toGameStateDto(entry).asJson))
|
||||
}
|
||||
|
||||
private def redoMove(id: String): IO[Response[IO]] =
|
||||
registry.get(id).flatMap {
|
||||
case None => notFound(id)
|
||||
case Some(entry) if !entry.engine.canRedo => badRequest("REDO_UNAVAILABLE", "No moves to redo")
|
||||
case Some(entry) =>
|
||||
IO.blocking { entry.engine.redo() }.flatMap(_ => Ok(toGameStateDto(entry).asJson))
|
||||
}
|
||||
|
||||
// ─── Draw handling ─────────────────────────────────────────────────────────
|
||||
|
||||
private def drawAction(id: String, action: String): IO[Response[IO]] =
|
||||
registry.get(id).flatMap {
|
||||
case None => notFound(id)
|
||||
case Some(entry) if entry.isOver => badRequest("GAME_OVER", "Game is already over")
|
||||
case Some(entry) => action match
|
||||
case "offer" =>
|
||||
IO.blocking { entry.drawOffered = true }.flatMap(_ => Ok(OkDto().asJson))
|
||||
case "decline" =>
|
||||
IO.blocking { entry.drawOffered = false }.flatMap(_ => Ok(OkDto().asJson))
|
||||
case "accept" =>
|
||||
if !entry.drawOffered then badRequest("NO_DRAW_OFFER", "No draw offer to accept")
|
||||
else IO.blocking {
|
||||
entry.gameOver = Some(GameOverState.Draw(entry.engine.context))
|
||||
}.flatMap(_ => Ok(OkDto().asJson))
|
||||
case "claim" =>
|
||||
IO.blocking {
|
||||
entry.lastEvent = None
|
||||
entry.engine.processUserInput("draw")
|
||||
entry.lastEvent
|
||||
}.flatMap {
|
||||
case Some(InvalidMoveEvent(_, reason)) => badRequest("DRAW_UNAVAILABLE", reason)
|
||||
case _ => Ok(OkDto().asJson)
|
||||
}
|
||||
case _ => badRequest("INVALID_ACTION", s"Unknown draw action: $action")
|
||||
}
|
||||
|
||||
// ─── Import / Export ───────────────────────────────────────────────────────
|
||||
|
||||
private def importFen(req: Request[IO]): IO[Response[IO]] =
|
||||
req.as[ImportFenRequest].flatMap { body =>
|
||||
FenParser.parseFen(body.fen) match
|
||||
case Left(err) => badRequest("INVALID_FEN", err)
|
||||
case Right(ctx) =>
|
||||
val white = resolvePlayer(body.white, "white")
|
||||
val black = resolvePlayer(body.black, "black")
|
||||
val entry = GameEntry(new GameEngine(ctx), white, black)
|
||||
registry.add(entry).flatMap { id => Created(toGameFullDto(id, entry).asJson) }
|
||||
}
|
||||
|
||||
private def importPgn(req: Request[IO]): IO[Response[IO]] =
|
||||
req.as[ImportPgnRequest].flatMap { body =>
|
||||
val engine = new GameEngine()
|
||||
engine.loadGame(PgnParser, body.pgn) match
|
||||
case Left(err) => badRequest("INVALID_PGN", err)
|
||||
case Right(_) =>
|
||||
val entry = GameEntry(engine, defaultPlayer("white"), defaultPlayer("black"))
|
||||
registry.add(entry).flatMap { id => Created(toGameFullDto(id, entry).asJson) }
|
||||
}
|
||||
|
||||
private def exportFen(id: String): IO[Response[IO]] =
|
||||
registry.get(id).flatMap {
|
||||
case None => notFound(id)
|
||||
case Some(entry) => Ok(FenExporter.gameContextToFen(entry.activeContext))
|
||||
}
|
||||
|
||||
private def exportPgn(id: String): IO[Response[IO]] =
|
||||
registry.get(id).flatMap {
|
||||
case None => notFound(id)
|
||||
case Some(entry) => Ok(PgnExporter.exportGameContext(entry.activeContext))
|
||||
}
|
||||
|
||||
// ─── Mapping helpers ───────────────────────────────────────────────────────
|
||||
|
||||
private def toGameStateDto(entry: GameEntry): GameStateDto =
|
||||
val ctx = entry.activeContext
|
||||
val (status, winner) = computeStatus(entry)
|
||||
GameStateDto(
|
||||
fen = FenExporter.gameContextToFen(ctx),
|
||||
pgn = PgnExporter.exportGameContext(ctx),
|
||||
turn = ctx.turn.toString.toLowerCase,
|
||||
status = status,
|
||||
winner = winner,
|
||||
moves = ctx.moves.map(moveToUci),
|
||||
undoAvailable = entry.engine.canUndo,
|
||||
redoAvailable = entry.engine.canRedo
|
||||
)
|
||||
|
||||
private def toGameFullDto(id: String, entry: GameEntry): GameFullDto =
|
||||
GameFullDto(
|
||||
gameId = id,
|
||||
white = PlayerInfoDto(entry.white.id.value, entry.white.displayName),
|
||||
black = PlayerInfoDto(entry.black.id.value, entry.black.displayName),
|
||||
state = toGameStateDto(entry)
|
||||
)
|
||||
|
||||
private def toLegalMoveDto(move: Move): LegalMoveDto =
|
||||
val (moveType, promo) = move.moveType match
|
||||
case MoveType.Normal(true) => ("capture", None)
|
||||
case MoveType.Normal(false) => ("normal", None)
|
||||
case MoveType.CastleKingside => ("castleKingside", None)
|
||||
case MoveType.CastleQueenside => ("castleQueenside", None)
|
||||
case MoveType.EnPassant => ("enPassant", None)
|
||||
case MoveType.Promotion(pp) =>
|
||||
val s = pp match
|
||||
case PromotionPiece.Queen => "queen"
|
||||
case PromotionPiece.Rook => "rook"
|
||||
case PromotionPiece.Bishop => "bishop"
|
||||
case PromotionPiece.Knight => "knight"
|
||||
("promotion", Some(s))
|
||||
LegalMoveDto(move.from.toString, move.to.toString, moveToUci(move), moveType, promo)
|
||||
|
||||
private def moveToUci(move: Move): String =
|
||||
val base = s"${move.from}${move.to}"
|
||||
move.moveType match
|
||||
case MoveType.Promotion(pp) =>
|
||||
val char = pp match
|
||||
case PromotionPiece.Queen => "q"
|
||||
case PromotionPiece.Rook => "r"
|
||||
case PromotionPiece.Bishop => "b"
|
||||
case PromotionPiece.Knight => "n"
|
||||
base + char
|
||||
case _ => base
|
||||
|
||||
private def applyUci(entry: GameEntry, uci: String): Unit =
|
||||
if uci.length == 5 then
|
||||
entry.engine.processUserInput(uci.take(4))
|
||||
if entry.engine.isPendingPromotion then
|
||||
val pp = uci.last match
|
||||
case 'q' => PromotionPiece.Queen
|
||||
case 'r' => PromotionPiece.Rook
|
||||
case 'b' => PromotionPiece.Bishop
|
||||
case _ => PromotionPiece.Knight
|
||||
entry.engine.completePromotion(pp)
|
||||
else
|
||||
entry.engine.processUserInput(uci)
|
||||
|
||||
private def computeStatus(entry: GameEntry): (String, Option[String]) =
|
||||
entry.gameOver match
|
||||
case Some(GameOverState.Checkmate(w, _)) => ("checkmate", Some(w.toString.toLowerCase))
|
||||
case Some(GameOverState.Stalemate(_)) => ("stalemate", None)
|
||||
case Some(GameOverState.Draw(_)) => ("draw", None)
|
||||
case Some(GameOverState.Resigned(w, _)) => ("resign", Some(w.toString.toLowerCase))
|
||||
case None => statusFromContext(entry)
|
||||
|
||||
private def statusFromContext(entry: GameEntry): (String, Option[String]) =
|
||||
if entry.engine.isPendingPromotion then ("promotionPending", None)
|
||||
else if entry.drawOffered then ("drawOffered", None)
|
||||
else
|
||||
val ctx = entry.engine.context
|
||||
val rs = entry.engine.ruleSet
|
||||
if rs.isInsufficientMaterial(ctx) then ("insufficientMaterial", None)
|
||||
else if rs.isFiftyMoveRule(ctx) then ("fiftyMoveAvailable", None)
|
||||
else if rs.isCheck(ctx) then ("check", None)
|
||||
else ("started", None)
|
||||
|
||||
// ─── Utility ───────────────────────────────────────────────────────────────
|
||||
|
||||
private def resolvePlayer(dto: Option[PlayerInfoDto], color: String): PlayerInfo =
|
||||
dto.map(d => PlayerInfo(PlayerId(d.id), d.displayName))
|
||||
.getOrElse(defaultPlayer(color))
|
||||
|
||||
private def defaultPlayer(color: String): PlayerInfo =
|
||||
PlayerInfo(PlayerId(color), color.capitalize)
|
||||
|
||||
private def notFound(id: String): IO[Response[IO]] =
|
||||
NotFound(ApiErrorDto("GAME_NOT_FOUND", s"Game not found: $id").asJson)
|
||||
|
||||
private def badRequest(code: String, msg: String): IO[Response[IO]] =
|
||||
BadRequest(ApiErrorDto(code, msg).asJson)
|
||||
@@ -0,0 +1,41 @@
|
||||
package de.nowchess.server
|
||||
|
||||
import de.nowchess.api.board.Color
|
||||
import de.nowchess.api.game.GameContext
|
||||
import de.nowchess.api.player.PlayerInfo
|
||||
import de.nowchess.chess.engine.GameEngine
|
||||
import de.nowchess.chess.observer.*
|
||||
|
||||
enum GameOverState:
|
||||
case Checkmate(winner: Color, context: GameContext)
|
||||
case Stalemate(context: GameContext)
|
||||
case Draw(context: GameContext)
|
||||
case Resigned(winner: Color, context: GameContext)
|
||||
|
||||
class GameEntry(
|
||||
val engine: GameEngine,
|
||||
val white: PlayerInfo,
|
||||
val black: PlayerInfo
|
||||
):
|
||||
var lastEvent: Option[GameEvent] = None
|
||||
var gameOver: Option[GameOverState] = None
|
||||
var drawOffered: Boolean = false
|
||||
|
||||
engine.subscribe(new Observer:
|
||||
def onGameEvent(event: GameEvent): Unit =
|
||||
lastEvent = Some(event)
|
||||
event match
|
||||
case CheckmateEvent(ctx, winner) => gameOver = Some(GameOverState.Checkmate(winner, ctx))
|
||||
case StalemateEvent(ctx) => gameOver = Some(GameOverState.Stalemate(ctx))
|
||||
case DrawClaimedEvent(ctx) => gameOver = Some(GameOverState.Draw(ctx))
|
||||
case _ => ()
|
||||
)
|
||||
|
||||
def isOver: Boolean = gameOver.isDefined
|
||||
|
||||
def activeContext: GameContext = gameOver match
|
||||
case Some(GameOverState.Checkmate(_, ctx)) => ctx
|
||||
case Some(GameOverState.Stalemate(ctx)) => ctx
|
||||
case Some(GameOverState.Draw(ctx)) => ctx
|
||||
case Some(GameOverState.Resigned(_, ctx)) => ctx
|
||||
case None => engine.context
|
||||
@@ -0,0 +1,21 @@
|
||||
package de.nowchess.server
|
||||
|
||||
import cats.effect.{IO, Ref}
|
||||
import de.nowchess.api.player.{PlayerId, PlayerInfo}
|
||||
import de.nowchess.chess.engine.GameEngine
|
||||
|
||||
class GameRegistry(ref: Ref[IO, Map[String, GameEntry]]):
|
||||
|
||||
def create(white: PlayerInfo, black: PlayerInfo): IO[String] =
|
||||
add(GameEntry(new GameEngine(), white, black))
|
||||
|
||||
def add(entry: GameEntry): IO[String] =
|
||||
val id = scala.util.Random.alphanumeric.take(8).mkString
|
||||
ref.update(_ + (id -> entry)).as(id)
|
||||
|
||||
def get(id: String): IO[Option[GameEntry]] =
|
||||
ref.get.map(_.get(id))
|
||||
|
||||
object GameRegistry:
|
||||
def make: IO[GameRegistry] =
|
||||
Ref.of[IO, Map[String, GameEntry]](Map.empty).map(new GameRegistry(_))
|
||||
@@ -0,0 +1,18 @@
|
||||
package de.nowchess.server
|
||||
|
||||
import io.circe.{Decoder, Encoder}
|
||||
import io.circe.generic.semiauto.*
|
||||
|
||||
object JsonCodecs:
|
||||
given Decoder[PlayerInfoDto] = deriveDecoder
|
||||
given Encoder[PlayerInfoDto] = deriveEncoder
|
||||
given Decoder[CreateGameRequest] = deriveDecoder
|
||||
given Decoder[ImportFenRequest] = deriveDecoder
|
||||
given Decoder[ImportPgnRequest] = deriveDecoder
|
||||
given Encoder[GameStateDto] = deriveEncoder
|
||||
given Encoder[GameFullDto] = deriveEncoder
|
||||
given Encoder[GameFullEvent] = deriveEncoder
|
||||
given Encoder[LegalMoveDto] = deriveEncoder
|
||||
given Encoder[LegalMovesDto] = deriveEncoder
|
||||
given Encoder[OkDto] = deriveEncoder
|
||||
given Encoder[ApiErrorDto] = deriveEncoder
|
||||
@@ -0,0 +1,54 @@
|
||||
package de.nowchess.server
|
||||
|
||||
case class PlayerInfoDto(id: String, displayName: String)
|
||||
|
||||
case class CreateGameRequest(
|
||||
white: Option[PlayerInfoDto] = None,
|
||||
black: Option[PlayerInfoDto] = None
|
||||
)
|
||||
|
||||
case class ImportFenRequest(
|
||||
fen: String,
|
||||
white: Option[PlayerInfoDto] = None,
|
||||
black: Option[PlayerInfoDto] = None
|
||||
)
|
||||
|
||||
case class ImportPgnRequest(pgn: String)
|
||||
|
||||
case class GameStateDto(
|
||||
fen: String,
|
||||
pgn: String,
|
||||
turn: String,
|
||||
status: String,
|
||||
winner: Option[String],
|
||||
moves: List[String],
|
||||
undoAvailable: Boolean,
|
||||
redoAvailable: Boolean
|
||||
)
|
||||
|
||||
case class GameFullDto(
|
||||
gameId: String,
|
||||
white: PlayerInfoDto,
|
||||
black: PlayerInfoDto,
|
||||
state: GameStateDto
|
||||
)
|
||||
|
||||
case class GameFullEvent(`type`: String, game: GameFullDto)
|
||||
|
||||
case class LegalMoveDto(
|
||||
from: String,
|
||||
to: String,
|
||||
uci: String,
|
||||
moveType: String,
|
||||
promotion: Option[String]
|
||||
)
|
||||
|
||||
case class LegalMovesDto(moves: List[LegalMoveDto])
|
||||
|
||||
case class OkDto(ok: Boolean = true)
|
||||
|
||||
case class ApiErrorDto(
|
||||
code: String,
|
||||
message: String,
|
||||
field: Option[String] = None
|
||||
)
|
||||
@@ -0,0 +1,17 @@
|
||||
package de.nowchess.server
|
||||
|
||||
import cats.effect.{ExitCode, IO, IOApp}
|
||||
import com.comcast.ip4s.{host, port}
|
||||
import org.http4s.ember.server.EmberServerBuilder
|
||||
|
||||
object ServerApp extends IOApp:
|
||||
def run(args: List[String]): IO[ExitCode] =
|
||||
GameRegistry.make.flatMap { registry =>
|
||||
EmberServerBuilder
|
||||
.default[IO]
|
||||
.withHost(host"0.0.0.0")
|
||||
.withPort(port"8080")
|
||||
.withHttpApp(BoardRoutes(registry).routes.orNotFound)
|
||||
.build
|
||||
.useForever
|
||||
}.as(ExitCode.Success)
|
||||
Reference in New Issue
Block a user