235 lines
8.9 KiB
Java
235 lines
8.9 KiB
Java
package games.dmg.spigottyrant;
|
|
|
|
import java.time.Duration;
|
|
import java.time.Instant;
|
|
import java.util.HashMap;
|
|
import java.util.Map;
|
|
import java.util.Optional;
|
|
import java.util.Set;
|
|
import java.util.UUID;
|
|
import java.util.random.RandomGenerator;
|
|
|
|
public final class RoleMaintenanceService {
|
|
private final RoleCandidateSelector candidates;
|
|
private final TyrantSuccessionService succession;
|
|
private final Duration inactivity;
|
|
private final Duration activityWindow;
|
|
private final Duration pendingTimeout;
|
|
private final RandomGenerator random;
|
|
|
|
public RoleMaintenanceService(
|
|
RoleCandidateSelector candidates,
|
|
TyrantSuccessionService succession,
|
|
Duration inactivity,
|
|
Duration activityWindow,
|
|
Duration pendingTimeout,
|
|
RandomGenerator random
|
|
) {
|
|
this.candidates = candidates;
|
|
this.succession = succession;
|
|
this.inactivity = inactivity;
|
|
this.activityWindow = activityWindow;
|
|
this.pendingTimeout = pendingTimeout;
|
|
this.random = random;
|
|
}
|
|
|
|
public LifecycleState maintain(
|
|
PersistentState state,
|
|
Set<UUID> onlinePlayerIds,
|
|
Instant now
|
|
) {
|
|
if (state.game().lifecycle() != GameLifecycle.RUNNING) {
|
|
return new LifecycleState(state.game(), state.players());
|
|
}
|
|
LifecycleState current = confirmPending(state, onlinePlayerIds, now);
|
|
if (isInactive(current.game().tyrantId(), current.players(), now)) {
|
|
return succession.succeed(
|
|
current.asPersistentState(), Optional.empty(), onlinePlayerIds, now
|
|
);
|
|
}
|
|
if (isInactive(current.game().vigilanteId(), current.players(), now)) {
|
|
current = rerollVigilante(
|
|
current.asPersistentState(), current.game().vigilanteId(), onlinePlayerIds, now
|
|
);
|
|
}
|
|
current = rerollExpiredTyrant(current, onlinePlayerIds, now);
|
|
current = rerollExpiredVigilante(current, onlinePlayerIds, now);
|
|
if (current.game().tyrantId().isEmpty() && current.game().pendingTyrant().isEmpty()) {
|
|
current = selectTyrant(current, Optional.empty(), onlinePlayerIds, now);
|
|
}
|
|
if (current.game().tyrantId().isPresent()
|
|
&& current.game().vigilanteId().isEmpty()
|
|
&& current.game().pendingVigilante().isEmpty()) {
|
|
current = rerollVigilante(
|
|
current.asPersistentState(), Optional.empty(), onlinePlayerIds, now
|
|
);
|
|
}
|
|
return current;
|
|
}
|
|
|
|
public LifecycleState rerollVigilante(
|
|
PersistentState state,
|
|
Optional<UUID> previousVigilante,
|
|
Set<UUID> onlinePlayerIds,
|
|
Instant now
|
|
) {
|
|
Map<UUID, PlayerState> players = clearFollowers(state.players(), previousVigilante);
|
|
Set<UUID> excluded = state.game().tyrantId().map(Set::of).orElseGet(Set::of);
|
|
Optional<UUID> selected = candidates.select(
|
|
players, excluded, previousVigilante, now, activityWindow, random
|
|
);
|
|
Optional<UUID> active = selected.filter(onlinePlayerIds::contains);
|
|
Optional<PendingSelection> pending = selected
|
|
.filter(playerId -> active.isEmpty())
|
|
.map(playerId -> new PendingSelection(playerId, now.plus(pendingTimeout)));
|
|
GameState game = copyRoles(
|
|
state.game(), state.game().tyrantId(), active,
|
|
state.game().pendingTyrant(), pending
|
|
);
|
|
return new LifecycleState(game, players);
|
|
}
|
|
|
|
private LifecycleState confirmPending(
|
|
PersistentState state,
|
|
Set<UUID> onlinePlayerIds,
|
|
Instant now
|
|
) {
|
|
GameState game = state.game();
|
|
Optional<UUID> tyrant = game.tyrantId();
|
|
Optional<UUID> vigilante = game.vigilanteId();
|
|
Optional<PendingSelection> pendingTyrant = game.pendingTyrant();
|
|
Optional<PendingSelection> pendingVigilante = game.pendingVigilante();
|
|
if (pendingTyrant.isPresent()) {
|
|
PendingSelection pending = pendingTyrant.orElseThrow();
|
|
if (!now.isAfter(pending.expiresAt())
|
|
&& onlinePlayerIds.contains(pending.candidateId())
|
|
&& eligibleAtConfirmation(state.players().get(pending.candidateId()), now)
|
|
&& vigilante.filter(pending.candidateId()::equals).isEmpty()) {
|
|
tyrant = Optional.of(pending.candidateId());
|
|
pendingTyrant = Optional.empty();
|
|
}
|
|
}
|
|
if (pendingVigilante.isPresent()) {
|
|
PendingSelection pending = pendingVigilante.orElseThrow();
|
|
if (!now.isAfter(pending.expiresAt())
|
|
&& onlinePlayerIds.contains(pending.candidateId())
|
|
&& eligibleAtConfirmation(state.players().get(pending.candidateId()), now)
|
|
&& tyrant.filter(pending.candidateId()::equals).isEmpty()) {
|
|
vigilante = Optional.of(pending.candidateId());
|
|
pendingVigilante = Optional.empty();
|
|
}
|
|
}
|
|
return new LifecycleState(
|
|
copyRoles(game, tyrant, vigilante, pendingTyrant, pendingVigilante),
|
|
state.players()
|
|
);
|
|
}
|
|
|
|
private LifecycleState rerollExpiredTyrant(
|
|
LifecycleState state,
|
|
Set<UUID> onlinePlayerIds,
|
|
Instant now
|
|
) {
|
|
Optional<PendingSelection> pending = state.game().pendingTyrant();
|
|
if (pending.isEmpty() || !now.isAfter(pending.orElseThrow().expiresAt())) {
|
|
return state;
|
|
}
|
|
return selectTyrant(
|
|
state,
|
|
Optional.of(pending.orElseThrow().candidateId()),
|
|
onlinePlayerIds,
|
|
now
|
|
);
|
|
}
|
|
|
|
private LifecycleState selectTyrant(
|
|
LifecycleState state,
|
|
Optional<UUID> previous,
|
|
Set<UUID> onlinePlayerIds,
|
|
Instant now
|
|
) {
|
|
Set<UUID> excluded = state.game().vigilanteId().map(Set::of).orElseGet(Set::of);
|
|
Optional<UUID> selected = candidates.select(
|
|
state.players(), excluded, previous, now, activityWindow, random
|
|
);
|
|
Optional<UUID> active = selected.filter(onlinePlayerIds::contains);
|
|
Optional<PendingSelection> pending = selected
|
|
.filter(playerId -> active.isEmpty())
|
|
.map(playerId -> new PendingSelection(playerId, now.plus(pendingTimeout)));
|
|
GameState game = copyRoles(
|
|
state.game(), active, state.game().vigilanteId(), pending,
|
|
state.game().pendingVigilante()
|
|
);
|
|
return new LifecycleState(game, state.players());
|
|
}
|
|
|
|
private LifecycleState rerollExpiredVigilante(
|
|
LifecycleState state,
|
|
Set<UUID> onlinePlayerIds,
|
|
Instant now
|
|
) {
|
|
Optional<PendingSelection> pending = state.game().pendingVigilante();
|
|
if (pending.isEmpty() || !now.isAfter(pending.orElseThrow().expiresAt())) {
|
|
return state;
|
|
}
|
|
return rerollVigilante(
|
|
state.asPersistentState(),
|
|
Optional.of(pending.orElseThrow().candidateId()),
|
|
onlinePlayerIds,
|
|
now
|
|
);
|
|
}
|
|
|
|
private boolean isInactive(
|
|
Optional<UUID> roleHolder,
|
|
Map<UUID, PlayerState> players,
|
|
Instant now
|
|
) {
|
|
if (roleHolder.isEmpty()) {
|
|
return false;
|
|
}
|
|
PlayerState player = players.get(roleHolder.orElseThrow());
|
|
return player != null
|
|
&& player.lastLogin().isPresent()
|
|
&& !player.lastLogin().orElseThrow().plus(inactivity).isAfter(now);
|
|
}
|
|
|
|
private static boolean eligibleAtConfirmation(PlayerState player, Instant now) {
|
|
return player != null && player.optedOutUntil().isEmpty();
|
|
}
|
|
|
|
private static Map<UUID, PlayerState> clearFollowers(
|
|
Map<UUID, PlayerState> players,
|
|
Optional<UUID> previousVigilante
|
|
) {
|
|
if (previousVigilante.isEmpty()) {
|
|
return players;
|
|
}
|
|
Map<UUID, PlayerState> updated = new HashMap<>();
|
|
UUID previous = previousVigilante.orElseThrow();
|
|
for (PlayerState player : players.values()) {
|
|
Optional<UUID> followerOf = player.followerOf().filter(id -> !id.equals(previous));
|
|
updated.put(player.playerId(), new PlayerState(
|
|
player.playerId(), player.latestName(), player.lastLogin(), player.optedOutUntil(),
|
|
player.tyrantClass(), followerOf, player.cooldownEnds(),
|
|
player.readyAbilityItems(), player.capturedMobs()
|
|
));
|
|
}
|
|
return Map.copyOf(updated);
|
|
}
|
|
|
|
private static GameState copyRoles(
|
|
GameState game,
|
|
Optional<UUID> tyrant,
|
|
Optional<UUID> vigilante,
|
|
Optional<PendingSelection> pendingTyrant,
|
|
Optional<PendingSelection> pendingVigilante
|
|
) {
|
|
return new GameState(
|
|
game.lifecycle(), tyrant, vigilante, pendingTyrant, pendingVigilante,
|
|
game.pausedAt(), game.accumulatedPausedTime(), game.tyrantLevel(),
|
|
game.unspentChoices(), game.purchases()
|
|
);
|
|
}
|
|
}
|