Skip to content

Match scoring

The me.zlex.conduit.match package gives a minigame everything it needs to score across rounds and persist the outcome:

  • Match, a pure-data best-of-N holder (round number + per-player point tally). No Minecraft references, so it’s trivially testable.
  • MatchResult, an immutable, persistable snapshot of a finished Match, with a commitTo(server) bridge into the core ProfileStore.
  • MatchScoreboard, drives an fx ScoreBoardBinding sidebar straight from a Match.
  • MatchHistorySavedData, appends results to a queryable recent-matches feed (cap MAX = 50).
  • Podium, builds a 1st/2nd/3rd podium, fires celebration fireworks, and shows a winner card.
  • MatchCompletedEvent, fired on the core ConduitEvents bus when a match finishes.
  • Your game runs multiple rounds and you want per-player scoring with a stable leaderboard.
  • You want wins / games-played / points folded into durable player profiles (ProfileStore).
  • You want a ready-made standings sidebar and end-of-match podium.
  • You’re writing a tournament runner that advances games when each Match finishes.
package me.zlex.conduit.match;
public final class Match {
public Match(int totalRounds); // totalRounds must be >= 1
public int totalRounds();
public int currentRound(); // 1-based
public boolean advanceRound(); // false once no rounds remain
public boolean isOver(); // currentRound >= totalRounds
public void award(UUID player, int delta);
public void register(UUID player);
public int pointsOf(UUID player);
public Map<UUID, Integer> points(); // unmodifiable view
public List<UUID> standings(); // best first; ties keep insertion order
public Optional<UUID> leader();
public Optional<UUID> winner(); // == leader(), named for end-of-match
public MatchResult finish(String gameId); // snapshots + fires MatchCompletedEvent
}
public record MatchResult(String gameId, Optional<UUID> winner, List<UUID> standings,
Map<UUID, Integer> points, int rounds, long endedAt) {
public static final Codec<MatchResult> CODEC;
public static MatchResult of(String gameId, Match m);
public void commitTo(MinecraftServer server); // wins / games_played / points_total
}
public final class MatchScoreboard {
public static ScoreBoardBinding attach(String title, Match match,
Function<UUID, String> nameOf,
Collection<ServerPlayer> audience);
public static List<String> rows(Match match, Function<UUID, String> nameOf);
}
public final class MatchHistorySavedData extends GameStateSavedData<MatchHistorySavedData> {
public static MatchHistorySavedData get(ServerLevel level);
public void record(MatchResult result);
public List<MatchResult> recent();
public List<MatchResult> recentFor(String gameId);
}
public final class Podium {
public static void celebrate(MinecraftServer server, ServerLevel level, BlockPos at,
List<UUID> top3, Function<UUID, String> nameOf);
public static void celebrate(MinecraftServer server, ServerLevel level, BlockPos at,
List<UUID> top3, Function<UUID, String> nameOf,
Collection<ServerPlayer> audience);
}
public record MatchCompletedEvent(String gameId, Optional<UUID> winner,
List<UUID> standings, MatchResult result)
implements ConduitEvent {}
import me.zlex.conduit.match.Match;
import me.zlex.conduit.match.MatchResult;
import me.zlex.conduit.match.MatchScoreboard;
import me.zlex.conduit.match.Podium;
import net.minecraft.core.BlockPos;
import net.minecraft.server.MinecraftServer;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.server.level.ServerPlayer;
import java.util.Collection;
import java.util.UUID;
import java.util.function.Function;
public final class MyMatchFlow {
private final Match match = new Match(5); // best-of-5
void startMatch(Collection<ServerPlayer> players) {
for (ServerPlayer p : players) match.register(p.getUUID());
MatchScoreboard.attach("§6Standings", match, this::nameOf, players);
}
void onRoundWin(UUID winner) {
match.award(winner, 1);
if (!match.advanceRound()) endMatch(); // false → no rounds remain
}
void endMatch() {
MinecraftServer server = inst.level().getServer();
MatchResult result = match.finish("hot-potato"); // also fires MatchCompletedEvent
result.commitTo(server); // fold into player profiles
Podium.celebrate(server, inst.level(), new BlockPos(0, 65, 0),
match.standings(), this::nameOf, lobbyMembers);
}
private String nameOf(UUID id) {
ServerPlayer p = server.getPlayerList().getPlayer(id);
return p != null ? p.getScoreboardName() : "?";
}
}
  • Match holds no Minecraft references: it keys players by UUID. The lobby maps those back to its own ServerPlayers (via your nameOf).
  • isOver() is true while the final round is still being played: it means currentRound >= totalRounds, not “the match is finished”. The canonical end-of-match signal is advanceRound() returning false.
  • match.finish(gameId) both snapshots a MatchResult and fires a MatchCompletedEvent on the bus. Listen for that event to drive a tournament / season runner.
  • MatchResult.commitTo(server) increments games_played for every participant, adds each score to points_total, and bumps the winner’s wins: call it once at end of match, on the server thread.
  • Podium.celebrate has a 6-arg overload taking an audience collection: the winner title card + fanfare are shown only to those players (the podium structure + fireworks stay visible to anyone nearby). Use it on multi-lobby servers so one lobby’s celebration doesn’t interrupt the rest.