Refactored Tournament to use matchExecutor and ResultBroadcaster. Added turnTime and players are now added through Tournament creation instead of on MatchMaker/ScoreSystem creation

This commit is contained in:
Bas de Jong
2026-01-12 08:33:54 +01:00
parent 5caf6900d1
commit a1f0d48477
14 changed files with 159 additions and 91 deletions

View File

@@ -2,11 +2,12 @@ package org.toop.framework.networking.server;
import org.toop.framework.gameFramework.model.game.TurnBasedGame;
import java.time.Duration;
import java.util.List;
import java.util.concurrent.CompletableFuture;
public interface GameServer<GAMETYPE, CLIENT, CHALLENGEIDTYPE> {
GameResultFuture startGame(String gameType, CLIENT... clients);
GameResultFuture startGame(String gameType, Duration turnTime, CLIENT... clients);
void addClient(CLIENT client);
void removeClient(CLIENT client);

View File

@@ -0,0 +1,10 @@
package org.toop.framework.networking.server;
import org.toop.framework.networking.server.client.NettyClient;
import java.time.Duration;
@FunctionalInterface
public interface MatchExecutor {
GameResultFuture submit(String gameType, Duration turnTime, NettyClient... clients);
}

View File

@@ -13,7 +13,7 @@ import org.toop.framework.networking.server.stores.TurnBasedGameTypeStore;
import org.toop.framework.networking.server.tournaments.*;
import org.toop.framework.networking.server.tournaments.matchmakers.RoundRobinMatchMaker;
import org.toop.framework.networking.server.tournaments.scoresystems.BasicScoreSystem;
import org.toop.framework.networking.server.tournaments.shufflers.RandomShuffle;
import org.toop.framework.networking.server.tournaments.scoresystems.IntegerScoreSystem;
import org.toop.framework.utils.ImmutablePair;
import java.util.*;
@@ -111,7 +111,7 @@ public class Server implements GameServer<TurnBasedGame, NettyClient, Long> {
public void acceptChallenge(Long challengeId) {
for (var challenge : gameChallenges) {
if (challenge.id() == challengeId) {
startGame(challenge.acceptChallenge(), challenge.getUsers());
startGame(challenge.acceptChallenge(), Duration.ofSeconds(10), challenge.getUsers());
break;
}
}
@@ -133,7 +133,7 @@ public class Server implements GameServer<TurnBasedGame, NettyClient, Long> {
}
@Override
public GameResultFuture startGame(String gameType, NettyClient... clients) {
public GameResultFuture startGame(String gameType, Duration turnTime, NettyClient... clients) {
if (!gameTypesStore.all().containsKey(gameType)) return null;
try {
@@ -250,7 +250,7 @@ public class Server implements GameServer<TurnBasedGame, NettyClient, Long> {
userNames.remove(first);
userNames.remove(second);
startGame(key, getUser(userLeft), getUser(userRight));
startGame(key, Duration.ofSeconds(10), getUser(userLeft), getUser(userRight));
}
}
}
@@ -294,33 +294,26 @@ public class Server implements GameServer<TurnBasedGame, NettyClient, Long> {
var tournamentUsers = new ArrayList<>(onlineUsers());
tournamentUsers.removeIf(admins::contains);
var matchMaker = new RoundRobinMatchMaker(tournamentUsers);
if (shuffle) {
matchMaker.shuffle(new RandomShuffle()); // Remove if not wanting to shuffle
}
Tournament tournament = new Tournament.Builder()
.server(this)
.matchExecutor(this::startGame)
.tournamentRunner(new AsyncTournamentRunner())
.matchMaker(matchMaker)
.scoreSystem(new BasicScoreSystem(tournamentUsers))
.matchMaker(new RoundRobinMatchMaker())
.scoreSystem(new BasicScoreSystem())
.resultBroadcaster(this::endTournament)
.turnTimeout(Duration.ofSeconds(5))
.addPlayers(tournamentUsers.toArray(NettyClient[]::new))
.addAdmins(admins.toArray(NettyClient[]::new))
.build();
try {
new Thread(() -> tournament.run(gameType)).start();
} catch (IllegalArgumentException e) {
admins.forEach(c -> c.send("ERR not enough clients to start a tournament"));
} catch (RuntimeException e) {
admins.forEach(c -> c.send("ERR no matches could be created to start a tournament with"));
}
new Thread(() -> tournament.run(gameType)).start();
}
public void endTournament(Map<NettyClient, Integer> score, String gameType) {
public void endTournament(IntegerScoreSystem score) {
List<String> u = new ArrayList<>();
List<Integer> s = new ArrayList<>();
for (var entry : score.entrySet()) {
for (var entry : score.getScore().entrySet()) {
u.add(entry.getKey().name());
s.add(entry.getValue());
}
@@ -332,7 +325,7 @@ public class Server implements GameServer<TurnBasedGame, NettyClient, Long> {
String msg = String.format(
"SVR RESULTS {GAMETYPE: \"%s\", USERS: %s, SCORES: %s, TOURNAMENT: 1}",
gameType,
"none", // TODO gametype
users,
scores
);

View File

@@ -1,11 +1,12 @@
package org.toop.framework.networking.server.tournaments;
import org.toop.framework.networking.server.GameResultFuture;
import org.toop.framework.networking.server.Server;
import org.toop.framework.networking.server.MatchExecutor;
import org.toop.framework.networking.server.client.NettyClient;
import org.toop.framework.networking.server.tournaments.matchmakers.MatchMaker;
import org.toop.framework.networking.server.tournaments.scoresystems.ScoreSystem;
import org.toop.framework.networking.server.tournaments.scoresystems.IntegerScoreSystem;
import java.time.Duration;
import java.util.*;
import java.util.concurrent.*;
@@ -13,9 +14,11 @@ public class AsyncTournamentRunner implements TournamentRunner {
@Override
public void run(
Server server,
MatchExecutor matchRunner,
MatchMaker matchMaker,
ScoreSystem scoreSystem,
IntegerScoreSystem scoreSystem,
ResultBroadcaster<IntegerScoreSystem> broadcaster,
Duration turnTime,
String gameType
) {
@@ -52,8 +55,8 @@ public class AsyncTournamentRunner implements TournamentRunner {
CompletableFuture<Void> f =
CompletableFuture.runAsync(() -> {
try {
GameResultFuture game = server.startGame(gameType, a, b);
scoreSystem.matchEndAwait(game);
GameResultFuture game = matchRunner.submit(gameType, turnTime, a, b);
scoreSystem.result(match, game.result().join());
} finally {
a.clearGame();
b.clearGame();
@@ -70,7 +73,7 @@ public class AsyncTournamentRunner implements TournamentRunner {
Thread.sleep(10); // Safety
}
server.endTournament(scoreSystem.getScore(), gameType);
broadcaster.broadcast(scoreSystem);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();

View File

@@ -1,28 +1,36 @@
package org.toop.framework.networking.server.tournaments;
import org.toop.framework.networking.server.GameResultFuture;
import org.toop.framework.networking.server.Server;
import org.toop.framework.networking.server.MatchExecutor;
import org.toop.framework.networking.server.tournaments.matchmakers.MatchMaker;
import org.toop.framework.networking.server.tournaments.scoresystems.ScoreSystem;
import org.toop.framework.networking.server.tournaments.scoresystems.IntegerScoreSystem;
import java.time.Duration;
import java.util.concurrent.*;
public class BasicTournamentRunner implements TournamentRunner {
@Override
public void run(Server server, MatchMaker matchMaker, ScoreSystem scoreSystem, String gameType) {
public void run(
MatchExecutor matchExecutor,
MatchMaker matchMaker,
IntegerScoreSystem scoreSystem,
ResultBroadcaster<IntegerScoreSystem> broadcaster,
Duration turnTime,
String gameType
) {
ExecutorService threadPool = Executors.newSingleThreadExecutor();
try {
threadPool.execute(() -> {
for (TournamentMatch match : matchMaker) {
// Play game and await the results
GameResultFuture game = server.startGame(gameType, match.getClient0(), match.getClient1());
scoreSystem.matchEndAwait(game);
GameResultFuture game = matchExecutor.submit(gameType, turnTime, match.getClient0(), match.getClient1());
scoreSystem.result(match, game.result().join());
match.getClient0().clearGame();
match.getClient1().clearGame();
}
server.endTournament(scoreSystem.getScore(), gameType);
broadcaster.broadcast(scoreSystem);
});
} finally {
threadPool.shutdown();

View File

@@ -0,0 +1,8 @@
package org.toop.framework.networking.server.tournaments;
import org.toop.framework.networking.server.tournaments.scoresystems.ScoreSystem;
@FunctionalInterface
public interface ResultBroadcaster<T extends ScoreSystem<?, ?, ?>> {
void broadcast(T scoreSystem);
}

View File

@@ -1,44 +1,62 @@
package org.toop.framework.networking.server.tournaments;
import org.toop.framework.networking.server.Server;
import org.toop.framework.networking.server.MatchExecutor;
import org.toop.framework.networking.server.client.NettyClient;
import org.toop.framework.networking.server.tournaments.matchmakers.MatchMaker;
import org.toop.framework.networking.server.tournaments.scoresystems.ScoreSystem;
import org.toop.framework.networking.server.tournaments.scoresystems.IntegerScoreSystem;
import java.time.Duration;
import java.util.Arrays;
import java.util.Objects;
public class Tournament {
private final Server server;
private final ScoreSystem scoreSystem;
private final MatchExecutor matchExecutor;
private final IntegerScoreSystem scoreSystem;
private final TournamentRunner tournamentRunner;
private final MatchMaker matchMaker;
private final ResultBroadcaster<IntegerScoreSystem> broadcaster;
private final NettyClient[] players;
private final Duration turnTime;
private Tournament(Tournament.Builder builder) {
server = builder.server;
matchExecutor = builder.matchExecutor;
scoreSystem = builder.scoreSystem;
tournamentRunner = builder.tournamentRunner;
matchMaker = builder.matchMaker;
broadcaster = builder.broadcaster;
players = builder.players;
turnTime = builder.turnTime;
}
public void run(String gameType) throws IllegalArgumentException {
if (server.gameTypes().stream().noneMatch(e -> e.equalsIgnoreCase(gameType)))
throw new IllegalArgumentException("Invalid game type");
tournamentRunner.run(server, matchMaker, scoreSystem, gameType);
Arrays.stream(players).forEach(e -> {
matchMaker.addPlayer(e);
scoreSystem.addPlayer(e);
});
tournamentRunner.run(matchExecutor, matchMaker, scoreSystem, broadcaster, turnTime, gameType);
}
public static class Builder {
private Server server;
private ScoreSystem scoreSystem;
private MatchExecutor matchExecutor;
private IntegerScoreSystem scoreSystem;
private TournamentRunner tournamentRunner;
private MatchMaker matchMaker;
private ResultBroadcaster<IntegerScoreSystem> broadcaster;
private NettyClient[] players;
private NettyClient[] observors;
private NettyClient[] admins;
private Duration turnTime = Duration.ofSeconds(10);
public Builder server(Server server) {
this.server = server;
public Builder matchExecutor(MatchExecutor matchExecutor) {
this.matchExecutor = matchExecutor;
return this;
}
public Builder scoreSystem(ScoreSystem scoreSystem) {
public Builder scoreSystem(IntegerScoreSystem scoreSystem) {
this.scoreSystem = scoreSystem;
return this;
}
@@ -53,11 +71,38 @@ public class Tournament {
return this;
}
public Builder resultBroadcaster(ResultBroadcaster<IntegerScoreSystem> broadcaster) {
this.broadcaster = broadcaster;
return this;
}
public Builder addPlayers(NettyClient[] players) {
this.players = players;
return this;
}
public Builder addObservers(NettyClient[] observors) { // TODO
this.observors = observors;
return this;
}
public Builder addAdmins(NettyClient[] admins) { // TODO
this.admins = admins;
return this;
}
public Builder turnTimeout(Duration turnTime) {
this.turnTime = turnTime;
return this;
}
public Tournament build() {
Objects.requireNonNull(server, "server");
Objects.requireNonNull(matchExecutor, "matchExecutor");
Objects.requireNonNull(scoreSystem, "scoreSystem");
Objects.requireNonNull(tournamentRunner, "tournamentRunner");
Objects.requireNonNull(matchMaker, "matchMaker");
Objects.requireNonNull(broadcaster, "resultBroadcaster"); // TODO is not always necessary and needs to be more generic, not just at the end
Objects.requireNonNull(players, "players");
return new Tournament(this);
}
}

View File

@@ -8,11 +8,11 @@ public class TournamentMatch extends ImmutablePair<NettyClient, NettyClient> {
super(a, b);
}
NettyClient getClient0() {
public NettyClient getClient0() {
return getLeft();
}
NettyClient getClient1() {
public NettyClient getClient1() {
return getRight();
}
}

View File

@@ -1,9 +1,12 @@
package org.toop.framework.networking.server.tournaments;
import org.toop.framework.networking.server.Server;
import org.toop.framework.networking.server.MatchExecutor;
import org.toop.framework.networking.server.tournaments.matchmakers.MatchMaker;
import org.toop.framework.networking.server.tournaments.scoresystems.ScoreSystem;
import org.toop.framework.networking.server.tournaments.scoresystems.IntegerScoreSystem;
import java.time.Duration;
public interface TournamentRunner {
void run(Server server, MatchMaker matchMaker, ScoreSystem scoreSystem, String gameType);
void run(MatchExecutor matchExecutor, MatchMaker matchMaker, IntegerScoreSystem scoreSystem,
ResultBroadcaster<IntegerScoreSystem> broadcaster, Duration turnTime, String gameType);
}

View File

@@ -7,6 +7,7 @@ import org.toop.framework.networking.server.tournaments.shufflers.Shuffler;
import java.util.List;
public interface MatchMaker extends Iterable<TournamentMatch> {
void addPlayer(NettyClient player);
List<NettyClient> getPlayers();
void shuffle(Shuffler shuffler);
}

View File

@@ -4,20 +4,25 @@ import org.toop.framework.networking.server.client.NettyClient;
import org.toop.framework.networking.server.tournaments.TournamentMatch;
import org.toop.framework.networking.server.tournaments.shufflers.Shuffler;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.NoSuchElementException;
public class RoundRobinMatchMaker implements MatchMaker {
private final List<NettyClient> players;
private final List<NettyClient> players = new ArrayList<>();
public RoundRobinMatchMaker(List<NettyClient> players) {
this.players = players;
public RoundRobinMatchMaker() {} // TODO let user decide store type
@Override
public void addPlayer(NettyClient player) {
players.addLast(player);
}
@Override
public void shuffle(Shuffler shuffler) {
if (players.size() < 2) return;
shuffler.shuffle(players);
}

View File

@@ -1,40 +1,35 @@
package org.toop.framework.networking.server.tournaments.scoresystems;
import org.toop.framework.networking.server.GameResultFuture;
import org.toop.framework.networking.server.client.NettyClient;
import org.toop.framework.networking.server.tournaments.TournamentMatch;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
public class BasicScoreSystem implements ScoreSystem {
public class BasicScoreSystem implements IntegerScoreSystem {
private final Map<NettyClient, Integer> scores = new ConcurrentHashMap<>();
private final int INIT_SCORE = 0;
private final int WIN_POINTS = 1;
public BasicScoreSystem(List<NettyClient> store) {
for (NettyClient c : store) {
scores.putIfAbsent(c, getInitScore());
}
public BasicScoreSystem() {} // TODO let user decide store type
@Override
public void addPlayer(NettyClient user) {
scores.putIfAbsent(user, INIT_SCORE);
}
@Override
public void matchEndAwait(GameResultFuture result) {
if (result.game().users().length < 2) return;
switch (result.result().join()) {
case 0 -> givePoints(result.game().users()[0]);
case 1 -> givePoints(result.game().users()[1]);
public void result(TournamentMatch match, Integer result) {
switch (result) {
case 0 -> scores.merge(match.getClient0(), WIN_POINTS, Integer::sum);
case 1 -> scores.merge(match.getClient1(), WIN_POINTS, Integer::sum);
case -1 -> {} // Draw
default -> {}
default -> throw new IllegalArgumentException("Unknown result: " + result);
}
}
private void givePoints(NettyClient client) {
int clientScore = scores.get(client);
scores.put(client, clientScore + getWinPointAmount());
}
@Override
public Map<NettyClient, Integer> getScore() {
return scores;

View File

@@ -0,0 +1,6 @@
package org.toop.framework.networking.server.tournaments.scoresystems;
import org.toop.framework.networking.server.client.NettyClient;
import org.toop.framework.networking.server.tournaments.TournamentMatch;
public interface IntegerScoreSystem extends ScoreSystem<TournamentMatch, Integer, NettyClient> {}

View File

@@ -1,19 +1,9 @@
package org.toop.framework.networking.server.tournaments.scoresystems;
import org.toop.framework.networking.server.GameResultFuture;
import org.toop.framework.networking.server.client.NettyClient;
import org.toop.framework.networking.server.tournaments.TournamentMatch;
import java.util.Map;
public interface ScoreSystem {
void matchEndAwait(GameResultFuture result);
Map<NettyClient, Integer> getScore();
default int getWinPointAmount() {
return 1;
}
default int getInitScore() {
return 0;
}
public interface ScoreSystem<MATCHTYPE, SCORETYPE, USERTYPE> {
void addPlayer(USERTYPE user);
void result(MATCHTYPE match, SCORETYPE result);
Map<USERTYPE, SCORETYPE> getScore();
}