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 org.toop.framework.gameFramework.model.game.TurnBasedGame;
import java.time.Duration;
import java.util.List; import java.util.List;
import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletableFuture;
public interface GameServer<GAMETYPE, CLIENT, CHALLENGEIDTYPE> { 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 addClient(CLIENT client);
void removeClient(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.*;
import org.toop.framework.networking.server.tournaments.matchmakers.RoundRobinMatchMaker; 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.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 org.toop.framework.utils.ImmutablePair;
import java.util.*; import java.util.*;
@@ -111,7 +111,7 @@ public class Server implements GameServer<TurnBasedGame, NettyClient, Long> {
public void acceptChallenge(Long challengeId) { public void acceptChallenge(Long challengeId) {
for (var challenge : gameChallenges) { for (var challenge : gameChallenges) {
if (challenge.id() == challengeId) { if (challenge.id() == challengeId) {
startGame(challenge.acceptChallenge(), challenge.getUsers()); startGame(challenge.acceptChallenge(), Duration.ofSeconds(10), challenge.getUsers());
break; break;
} }
} }
@@ -133,7 +133,7 @@ public class Server implements GameServer<TurnBasedGame, NettyClient, Long> {
} }
@Override @Override
public GameResultFuture startGame(String gameType, NettyClient... clients) { public GameResultFuture startGame(String gameType, Duration turnTime, NettyClient... clients) {
if (!gameTypesStore.all().containsKey(gameType)) return null; if (!gameTypesStore.all().containsKey(gameType)) return null;
try { try {
@@ -250,7 +250,7 @@ public class Server implements GameServer<TurnBasedGame, NettyClient, Long> {
userNames.remove(first); userNames.remove(first);
userNames.remove(second); 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()); var tournamentUsers = new ArrayList<>(onlineUsers());
tournamentUsers.removeIf(admins::contains); 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() Tournament tournament = new Tournament.Builder()
.server(this) .matchExecutor(this::startGame)
.tournamentRunner(new AsyncTournamentRunner()) .tournamentRunner(new AsyncTournamentRunner())
.matchMaker(matchMaker) .matchMaker(new RoundRobinMatchMaker())
.scoreSystem(new BasicScoreSystem(tournamentUsers)) .scoreSystem(new BasicScoreSystem())
.resultBroadcaster(this::endTournament)
.turnTimeout(Duration.ofSeconds(5))
.addPlayers(tournamentUsers.toArray(NettyClient[]::new))
.addAdmins(admins.toArray(NettyClient[]::new))
.build(); .build();
try { new Thread(() -> tournament.run(gameType)).start();
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"));
}
} }
public void endTournament(Map<NettyClient, Integer> score, String gameType) { public void endTournament(IntegerScoreSystem score) {
List<String> u = new ArrayList<>(); List<String> u = new ArrayList<>();
List<Integer> s = new ArrayList<>(); List<Integer> s = new ArrayList<>();
for (var entry : score.entrySet()) { for (var entry : score.getScore().entrySet()) {
u.add(entry.getKey().name()); u.add(entry.getKey().name());
s.add(entry.getValue()); s.add(entry.getValue());
} }
@@ -332,7 +325,7 @@ public class Server implements GameServer<TurnBasedGame, NettyClient, Long> {
String msg = String.format( String msg = String.format(
"SVR RESULTS {GAMETYPE: \"%s\", USERS: %s, SCORES: %s, TOURNAMENT: 1}", "SVR RESULTS {GAMETYPE: \"%s\", USERS: %s, SCORES: %s, TOURNAMENT: 1}",
gameType, "none", // TODO gametype
users, users,
scores scores
); );

View File

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

View File

@@ -1,28 +1,36 @@
package org.toop.framework.networking.server.tournaments; package org.toop.framework.networking.server.tournaments;
import org.toop.framework.networking.server.GameResultFuture; 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.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.*; import java.util.concurrent.*;
public class BasicTournamentRunner implements TournamentRunner { public class BasicTournamentRunner implements TournamentRunner {
@Override @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(); ExecutorService threadPool = Executors.newSingleThreadExecutor();
try { try {
threadPool.execute(() -> { threadPool.execute(() -> {
for (TournamentMatch match : matchMaker) { for (TournamentMatch match : matchMaker) {
// Play game and await the results // Play game and await the results
GameResultFuture game = server.startGame(gameType, match.getClient0(), match.getClient1()); GameResultFuture game = matchExecutor.submit(gameType, turnTime, match.getClient0(), match.getClient1());
scoreSystem.matchEndAwait(game); scoreSystem.result(match, game.result().join());
match.getClient0().clearGame(); match.getClient0().clearGame();
match.getClient1().clearGame(); match.getClient1().clearGame();
} }
server.endTournament(scoreSystem.getScore(), gameType); broadcaster.broadcast(scoreSystem);
}); });
} finally { } finally {
threadPool.shutdown(); 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; 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.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; import java.util.Objects;
public class Tournament { 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 TournamentRunner tournamentRunner;
private final MatchMaker matchMaker; private final MatchMaker matchMaker;
private final ResultBroadcaster<IntegerScoreSystem> broadcaster;
private final NettyClient[] players;
private final Duration turnTime;
private Tournament(Tournament.Builder builder) { private Tournament(Tournament.Builder builder) {
server = builder.server; matchExecutor = builder.matchExecutor;
scoreSystem = builder.scoreSystem; scoreSystem = builder.scoreSystem;
tournamentRunner = builder.tournamentRunner; tournamentRunner = builder.tournamentRunner;
matchMaker = builder.matchMaker; matchMaker = builder.matchMaker;
broadcaster = builder.broadcaster;
players = builder.players;
turnTime = builder.turnTime;
} }
public void run(String gameType) throws IllegalArgumentException { 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 { public static class Builder {
private Server server; private MatchExecutor matchExecutor;
private ScoreSystem scoreSystem; private IntegerScoreSystem scoreSystem;
private TournamentRunner tournamentRunner; private TournamentRunner tournamentRunner;
private MatchMaker matchMaker; 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) { public Builder matchExecutor(MatchExecutor matchExecutor) {
this.server = server; this.matchExecutor = matchExecutor;
return this; return this;
} }
public Builder scoreSystem(ScoreSystem scoreSystem) { public Builder scoreSystem(IntegerScoreSystem scoreSystem) {
this.scoreSystem = scoreSystem; this.scoreSystem = scoreSystem;
return this; return this;
} }
@@ -53,11 +71,38 @@ public class Tournament {
return this; 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() { public Tournament build() {
Objects.requireNonNull(server, "server"); Objects.requireNonNull(matchExecutor, "matchExecutor");
Objects.requireNonNull(scoreSystem, "scoreSystem"); Objects.requireNonNull(scoreSystem, "scoreSystem");
Objects.requireNonNull(tournamentRunner, "tournamentRunner"); Objects.requireNonNull(tournamentRunner, "tournamentRunner");
Objects.requireNonNull(matchMaker, "matchMaker"); 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); return new Tournament(this);
} }
} }

View File

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

View File

@@ -1,9 +1,12 @@
package org.toop.framework.networking.server.tournaments; 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.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 { 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; import java.util.List;
public interface MatchMaker extends Iterable<TournamentMatch> { public interface MatchMaker extends Iterable<TournamentMatch> {
void addPlayer(NettyClient player);
List<NettyClient> getPlayers(); List<NettyClient> getPlayers();
void shuffle(Shuffler shuffler); 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.TournamentMatch;
import org.toop.framework.networking.server.tournaments.shufflers.Shuffler; import org.toop.framework.networking.server.tournaments.shufflers.Shuffler;
import java.util.ArrayList;
import java.util.Iterator; import java.util.Iterator;
import java.util.List; import java.util.List;
import java.util.NoSuchElementException; import java.util.NoSuchElementException;
public class RoundRobinMatchMaker implements MatchMaker { public class RoundRobinMatchMaker implements MatchMaker {
private final List<NettyClient> players; private final List<NettyClient> players = new ArrayList<>();
public RoundRobinMatchMaker(List<NettyClient> players) { public RoundRobinMatchMaker() {} // TODO let user decide store type
this.players = players;
@Override
public void addPlayer(NettyClient player) {
players.addLast(player);
} }
@Override @Override
public void shuffle(Shuffler shuffler) { public void shuffle(Shuffler shuffler) {
if (players.size() < 2) return;
shuffler.shuffle(players); shuffler.shuffle(players);
} }

View File

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