Skip to content

Commit

Permalink
Progress with a sample automated player
Browse files Browse the repository at this point in the history
  • Loading branch information
blacelle committed Sep 12, 2024
1 parent 01dc300 commit 35243a3
Show file tree
Hide file tree
Showing 44 changed files with 610 additions and 151 deletions.
3 changes: 2 additions & 1 deletion Procfile
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
# https://devcenter.heroku.com/articles/java-support#default-web-process-type
web: java -Dserver.port=$PORT $JAVA_OPTS -jar server/target/*.jar
web: java -Dserver.port=$PORT $JAVA_OPTS -jar server/target/*-exec.jar
player: java $JAVA_OPTS -jar player/target/*-exec.jar
1 change: 0 additions & 1 deletion monolith/.gitignore

This file was deleted.

19 changes: 11 additions & 8 deletions monolith/src/main/resources/application.yml
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@

spring:
profiles:
group:
default:
# Renamed to enable merging in monolith
- "default_player"
- "default_server"
- "server"
- "fake_player"
- inject_default_games
include:
# Do not include `default`, as by `default` we include `fake_player`
- default_server
- default_player
group:
# `default` is used for quick-start: we enable fake security
default:
- fake_server
- fake_player
default_server:
- inject_default_games
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,10 @@
import org.springframework.test.context.TestPropertySource;
import org.springframework.test.context.junit.jupiter.SpringExtension;

import eu.solven.kumite.app.IKumiteServer;
import eu.solven.kumite.app.IKumiteSpringProfiles;
import eu.solven.kumite.app.KumiteServerApplication;
import eu.solven.kumite.app.KumiteWebclientServer;
import eu.solven.kumite.app.server.IKumiteServer;
import eu.solven.kumite.app.server.KumiteWebclientServer;
import eu.solven.kumite.contest.ContestSearchParameters;
import eu.solven.kumite.game.GameSearchParameters;
import eu.solven.kumite.player.PlayerRawMovesHolder;
Expand All @@ -35,7 +35,7 @@
// Should this move to `monolith` module?
@ExtendWith(SpringExtension.class)
@SpringBootTest(classes = KumiteServerApplication.class, webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@ActiveProfiles({ IKumiteSpringProfiles.P_DEFAULT, IKumiteSpringProfiles.P_DEFAULT_FAKE_PLAYER, })
@ActiveProfiles({ IKumiteSpringProfiles.P_DEFAULT, IKumiteSpringProfiles.P_FAKE_PLAYER, })
@TestPropertySource(properties = { "kumite.random.seed=123",
"kumite.server.base-url=http://localhost:LocalServerPort",
"kumite.random.seed=123" })
Expand Down
1 change: 0 additions & 1 deletion player/.gitignore

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,26 +1,20 @@
package eu.solven.kumite.app;

import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentSkipListSet;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.stream.Stream;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.core.env.Environment;

import eu.solven.kumite.contest.ContestSearchParameters;
import eu.solven.kumite.contest.ContestView;
import eu.solven.kumite.game.GameSearchParameters;
import eu.solven.kumite.app.player.IKumitePlayer;
import eu.solven.kumite.app.player.KumitePlayer;
import eu.solven.kumite.app.server.IKumiteServer;
import eu.solven.kumite.app.server.KumiteWebclientServer;
import lombok.extern.slf4j.Slf4j;
import reactor.core.publisher.Flux;

@Configuration
@Import({
Expand All @@ -37,51 +31,38 @@ public IKumiteServer kumiteServer(Environment env) {
}

@Bean
public Void playTicTacToe(IKumiteServer kumiteServer, Environment env) {
public IKumitePlayer kumitePlayer(IKumiteServer kumiteServer) {
return new KumitePlayer(kumiteServer);
}

@Bean
public Void playTicTacToe(IKumitePlayer kumitePlayer, Environment env) {
UUID playerId = env.getRequiredProperty("kumite.playerId", UUID.class);

ScheduledExecutorService ses = Executors.newScheduledThreadPool(4);

Map<UUID, ContestView> contestToDetails = new ConcurrentHashMap<>();
Set<UUID> playingContests = new ConcurrentSkipListSet<>();

Stream.of("Travelling Salesman Problem", "Tic-Tac-Toe").forEach(gameTitle -> {
ses.scheduleWithFixedDelay(() -> {
log.info("Looking for interesting contests for game LIKE `{}`", gameTitle);
kumiteServer.searchGames(GameSearchParameters.builder().titleRegex(Optional.of(gameTitle)).build())
.collectList()
.flatMapMany(games -> {
log.info("Games for `{}`: {}", gameTitle, games);
return Flux.fromStream(games.stream());
})
.flatMap(game -> kumiteServer.searchContests(
ContestSearchParameters.builder().gameId(Optional.of(game.getGameId())).build()))
.flatMap(contest -> kumiteServer.loadBoard(playerId, contest.getContestId()))
.filter(c -> !c.getDynamicMetadata().isGameOver())
.filter(c -> c.getDynamicMetadata().isAcceptingPlayers())
.doOnNext(contestView -> {
UUID contestId = contestView.getContestId();

if (contestView.getPlayingPlayer().isPlayerHasJoined()) {
log.info("Received board for already joined contestId={}", contestId);

playingContests.add(contestId);
contestToDetails.put(contestId, contestView);
} else if (contestView.getPlayingPlayer().isPlayerCanJoin()) {
log.info("Received board for joinable contestId={}", contestId);
kumiteServer.joinContest(playerId, contestId).subscribe(view -> {
log.info("Received board for joined contestId={}", contestId);
contestToDetails.put(contestId, contestView);
});
}
})
.subscribe(view -> {
log.info("View: {}", view);
});
}, 1, 60, TimeUnit.SECONDS);
});
ses.scheduleWithFixedDelay(() -> {
try {
log.info("Playing contests as {}", playerId);
kumitePlayer.playOptimizationGames(playerId);
} catch (Throwable t) {
log.warn("Issue while playing games", t);
}

}, 1, 60, TimeUnit.SECONDS);

ses.scheduleWithFixedDelay(() -> {
try {
log.info("Playing contests as {}", playerId);
kumitePlayer.play1v1(playerId);
} catch (Throwable t) {
log.warn("Issue while playing games", t);
}

}, 1, 60, TimeUnit.SECONDS);

return null;
}


}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package eu.solven.kumite.app.player;

import java.util.UUID;

public interface IKumitePlayer {

void playOptimizationGames(UUID playerId);

void play1v1(UUID playerId);

}
189 changes: 189 additions & 0 deletions player/src/main/java/eu/solven/kumite/app/player/KumitePlayer.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
package eu.solven.kumite.app.player;

import java.time.Duration;
import java.util.Map;
import java.util.Optional;
import java.util.UUID;

import eu.solven.kumite.app.server.IKumiteServer;
import eu.solven.kumite.contest.ContestSearchParameters;
import eu.solven.kumite.contest.ContestView;
import eu.solven.kumite.game.GameSearchParameters;
import eu.solven.kumite.game.IGameMetadataConstants;
import eu.solven.kumite.player.PlayerRawMovesHolder;
import lombok.AllArgsConstructor;
import lombok.NonNull;
import lombok.extern.slf4j.Slf4j;
import reactor.core.publisher.Mono;

@AllArgsConstructor
@Slf4j
public class KumitePlayer implements IKumitePlayer {
final IKumiteServer kumiteServer;

/**
* Optimization games are the simplest one in term of integration: one just have to publish one solution to get on
* the leaderboard
*
* @param kumiteServer
* @param playerId
*/
@Override
public void playOptimizationGames(UUID playerId) {
GameSearchParameters optimizationsGameSearch =
GameSearchParameters.builder().requiredTag(IGameMetadataConstants.TAG_OPTIMIZATION).build();

// Given a list of human-friendly game title
// Flux.fromStream(Stream.of("Travelling Salesman Problem", "Tic-Tac-Toe"))
// Build a SearchGames query
// .map(gameTitle -> ))
Mono.just(optimizationsGameSearch)
// Search the games
.flatMapMany(gameSearch -> {
log.info("Looking for games matching `{}`", gameSearch);
return kumiteServer.searchGames(gameSearch);
})
// Search for contests for given game
.flatMap(game -> kumiteServer.searchContests(
ContestSearchParameters.builder().gameId(Optional.of(game.getGameId())).build()))
// Load the board for given contest
.flatMap(contest -> kumiteServer.loadBoard(playerId, contest.getContestId()))
// Filter interesting boards
.filter(c -> !c.getDynamicMetadata().isGameOver())
.filter(c -> c.getDynamicMetadata().isAcceptingPlayers())
// Process each contest
.flatMap(contestView -> {
UUID contestId = contestView.getContestId();

if (contestView.getPlayingPlayer().isPlayerHasJoined()) {
log.info("Received board for already joined contestId={}", contestId);
return Mono.empty();
} else if (contestView.getPlayingPlayer().isPlayerCanJoin()) {
log.info("Received board for joinable contestId={}", contestId);
return kumiteServer.joinContest(playerId, contestId)
// We load the board again once we are signed-up
.flatMap(playingPlayer -> kumiteServer.loadBoard(contestId, playerId));
} else {
log.info("We can not join contest={}", contestId);
return Mono.empty();
}
})
.flatMap(joinedContestView -> {
UUID contestId = joinedContestView.getContestId();

if (joinedContestView.getDynamicMetadata().isGameOver()) {
log.info("contestId={} is gameOver", contestId);
return Mono.empty();
}

Mono<PlayerRawMovesHolder> exampleMoves =
kumiteServer.getExampleMoves(joinedContestView.getPlayingPlayer().getPlayerId(), contestId);

return exampleMoves.flatMap(moves -> {
Optional<Map<String, ?>> selectedMove = selectMove(joinedContestView.getBoard(), moves);

if (selectedMove.isEmpty()) {
log.info("No move. We quit the game. contestId=", contestId);
return Mono.empty();
}

return kumiteServer.playMove(playerId, joinedContestView.getContestId(), selectedMove.get());
});
})
.flatMap(contestView -> {
return kumiteServer.loadLeaderboard(contestView.getContestId()).doOnNext(leaderboard -> {
log.info("contestid={} leaderbord={}", contestView.getContestId(), leaderboard);
});
})
.subscribe();
}

/**
* 1v1 games needs sone sort of `do-while` loop, to play moves until the game is over.
*
* @param kumiteServer
* @param playerId
*/
@Override
public void play1v1(UUID playerId) {
GameSearchParameters optimizationsGameSearch =
GameSearchParameters.builder().requiredTag(IGameMetadataConstants.TAG_1V1).build();

Mono.just(optimizationsGameSearch)
// Search the games
.flatMapMany(gameSearch -> {
log.info("Looking for games matching `{}`", gameSearch);
return kumiteServer.searchGames(gameSearch);
})
// Search for contests for given game
.flatMap(game -> kumiteServer.searchContests(
ContestSearchParameters.builder().gameId(Optional.of(game.getGameId())).build()))
// Load the board for given contest
.flatMap(contest -> kumiteServer.loadBoard(playerId, contest.getContestId()))
// Filter interesting boards
.filter(c -> !c.getDynamicMetadata().isGameOver())
.filter(c -> c.getDynamicMetadata().isAcceptingPlayers())
// Process each contest
.flatMap(contestView -> {
UUID contestId = contestView.getContestId();

if (contestView.getPlayingPlayer().isPlayerHasJoined()) {
log.info("Received board for already joined contestId={}", contestId);
return Mono.empty();
} else if (contestView.getPlayingPlayer().isPlayerCanJoin()) {
log.info("Received board for joinable contestId={}", contestId);
return kumiteServer.joinContest(playerId, contestId)
.flatMap(playingPlayer -> kumiteServer.loadBoard(contestId, playerId));
} else {
log.info("We can not join contest={}", contestId);
return Mono.empty();
}
})
// This acts like a `do-while` loop: we loop by playing moves until the game is over
// https://codersee.com/project-reactor-expand/
.expand(joinedContestView -> {
UUID contestId = joinedContestView.getContestId();

if (joinedContestView.getDynamicMetadata().isGameOver()) {
log.info("contestId={} is gameOver", contestId);
return Mono.empty();
}

Mono<PlayerRawMovesHolder> exampleMoves =
kumiteServer.getExampleMoves(joinedContestView.getPlayingPlayer().getPlayerId(), contestId);
Mono<ContestView> monoContestViewPostMove = exampleMoves.flatMap(moves -> {
Optional<Map<String, ?>> optSelectedMove = selectMove(joinedContestView.getBoard(), moves);

if (optSelectedMove.isEmpty()) {
// There is no available move: wait until gameOver
Duration delay = Duration.ofSeconds(5);
log.info("No move. We wait {} for {}", delay, contestId);
return Mono.just(joinedContestView).delayElement(delay);
}

Map<String, ?> selectedMove = optSelectedMove.get();
log.info("We playMove `{}`for {}", selectedMove);
return kumiteServer.playMove(playerId, joinedContestView.getContestId(), selectedMove);
});
return monoContestViewPostMove;
})
.flatMap(contestView -> {
return kumiteServer.loadLeaderboard(contestView.getContestId()).doOnNext(leaderboard -> {
log.info("contestid={} leaderbord={}", contestView.getContestId(), leaderboard);
});
})
.subscribe();
}

/**
* Select one move amongst the examples moves.
*
* @param board
* @param moves
* @return the first suggested move.
*/
private Optional<Map<String, ?>> selectMove(@NonNull Map<String, ?> board, PlayerRawMovesHolder moves) {
// WaitForPlayersMove and WaitForSignups would have a `wait:true` flag
return moves.getMoves().values().stream().filter(m -> !Boolean.TRUE.equals(m.get("wait"))).findAny();
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package eu.solven.kumite.app;
package eu.solven.kumite.app.server;

import java.util.Map;
import java.util.UUID;
Expand All @@ -8,6 +8,7 @@
import eu.solven.kumite.contest.ContestView;
import eu.solven.kumite.game.GameMetadata;
import eu.solven.kumite.game.GameSearchParameters;
import eu.solven.kumite.leaderboard.LeaderBoardRaw;
import eu.solven.kumite.player.PlayerRawMovesHolder;
import eu.solven.kumite.player.PlayingPlayer;
import reactor.core.publisher.Flux;
Expand All @@ -18,12 +19,15 @@ public interface IKumiteServer {

Flux<ContestMetadataRaw> searchContests(ContestSearchParameters contestSearchParameters);

Mono<ContestView> loadBoard(UUID contestId, UUID playerId);
Mono<ContestView> loadBoard(UUID playerId, UUID contestId);

Mono<PlayingPlayer> joinContest(UUID playerId, UUID contestId);

Mono<PlayerRawMovesHolder> getExampleMoves(UUID playerId, UUID contestId);

// We may want not to receive the board, for optimization reasons.
Mono<ContestView> playMove(UUID playerId, UUID contestId, Map<String, ?> move);

Mono<LeaderBoardRaw> loadLeaderboard(UUID contestId);

}
Loading

0 comments on commit 35243a3

Please sign in to comment.