Skip to content

Commit 35243a3

Browse files
committed
Progress with a sample automated player
1 parent 01dc300 commit 35243a3

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

44 files changed

+610
-151
lines changed

Procfile

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
# https://devcenter.heroku.com/articles/java-support#default-web-process-type
2-
web: java -Dserver.port=$PORT $JAVA_OPTS -jar server/target/*.jar
2+
web: java -Dserver.port=$PORT $JAVA_OPTS -jar server/target/*-exec.jar
3+
player: java $JAVA_OPTS -jar player/target/*-exec.jar

monolith/.gitignore

-1
This file was deleted.
+11-8
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
11

22
spring:
33
profiles:
4-
group:
5-
default:
6-
# Renamed to enable merging in monolith
7-
- "default_player"
8-
- "default_server"
9-
- "server"
10-
- "fake_player"
11-
- inject_default_games
4+
include:
5+
# Do not include `default`, as by `default` we include `fake_player`
6+
- default_server
7+
- default_player
8+
group:
9+
# `default` is used for quick-start: we enable fake security
10+
default:
11+
- fake_server
12+
- fake_player
13+
default_server:
14+
- inject_default_games

monolith/src/test/java/eu/solven/kumite/app/it/TestTSPLifecycleThroughRouter.java

+3-3
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,10 @@
1414
import org.springframework.test.context.TestPropertySource;
1515
import org.springframework.test.context.junit.jupiter.SpringExtension;
1616

17-
import eu.solven.kumite.app.IKumiteServer;
1817
import eu.solven.kumite.app.IKumiteSpringProfiles;
1918
import eu.solven.kumite.app.KumiteServerApplication;
20-
import eu.solven.kumite.app.KumiteWebclientServer;
19+
import eu.solven.kumite.app.server.IKumiteServer;
20+
import eu.solven.kumite.app.server.KumiteWebclientServer;
2121
import eu.solven.kumite.contest.ContestSearchParameters;
2222
import eu.solven.kumite.game.GameSearchParameters;
2323
import eu.solven.kumite.player.PlayerRawMovesHolder;
@@ -35,7 +35,7 @@
3535
// Should this move to `monolith` module?
3636
@ExtendWith(SpringExtension.class)
3737
@SpringBootTest(classes = KumiteServerApplication.class, webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
38-
@ActiveProfiles({ IKumiteSpringProfiles.P_DEFAULT, IKumiteSpringProfiles.P_DEFAULT_FAKE_PLAYER, })
38+
@ActiveProfiles({ IKumiteSpringProfiles.P_DEFAULT, IKumiteSpringProfiles.P_FAKE_PLAYER, })
3939
@TestPropertySource(properties = { "kumite.random.seed=123",
4040
"kumite.server.base-url=http://localhost:LocalServerPort",
4141
"kumite.random.seed=123" })

player/.gitignore

-1
This file was deleted.
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,20 @@
11
package eu.solven.kumite.app;
22

3-
import java.util.Map;
4-
import java.util.Optional;
5-
import java.util.Set;
63
import java.util.UUID;
7-
import java.util.concurrent.ConcurrentHashMap;
8-
import java.util.concurrent.ConcurrentSkipListSet;
94
import java.util.concurrent.Executors;
105
import java.util.concurrent.ScheduledExecutorService;
116
import java.util.concurrent.TimeUnit;
12-
import java.util.stream.Stream;
137

148
import org.springframework.context.annotation.Bean;
159
import org.springframework.context.annotation.Configuration;
1610
import org.springframework.context.annotation.Import;
1711
import org.springframework.core.env.Environment;
1812

19-
import eu.solven.kumite.contest.ContestSearchParameters;
20-
import eu.solven.kumite.contest.ContestView;
21-
import eu.solven.kumite.game.GameSearchParameters;
13+
import eu.solven.kumite.app.player.IKumitePlayer;
14+
import eu.solven.kumite.app.player.KumitePlayer;
15+
import eu.solven.kumite.app.server.IKumiteServer;
16+
import eu.solven.kumite.app.server.KumiteWebclientServer;
2217
import lombok.extern.slf4j.Slf4j;
23-
import reactor.core.publisher.Flux;
2418

2519
@Configuration
2620
@Import({
@@ -37,51 +31,38 @@ public IKumiteServer kumiteServer(Environment env) {
3731
}
3832

3933
@Bean
40-
public Void playTicTacToe(IKumiteServer kumiteServer, Environment env) {
34+
public IKumitePlayer kumitePlayer(IKumiteServer kumiteServer) {
35+
return new KumitePlayer(kumiteServer);
36+
}
37+
38+
@Bean
39+
public Void playTicTacToe(IKumitePlayer kumitePlayer, Environment env) {
4140
UUID playerId = env.getRequiredProperty("kumite.playerId", UUID.class);
4241

4342
ScheduledExecutorService ses = Executors.newScheduledThreadPool(4);
4443

45-
Map<UUID, ContestView> contestToDetails = new ConcurrentHashMap<>();
46-
Set<UUID> playingContests = new ConcurrentSkipListSet<>();
47-
48-
Stream.of("Travelling Salesman Problem", "Tic-Tac-Toe").forEach(gameTitle -> {
49-
ses.scheduleWithFixedDelay(() -> {
50-
log.info("Looking for interesting contests for game LIKE `{}`", gameTitle);
51-
kumiteServer.searchGames(GameSearchParameters.builder().titleRegex(Optional.of(gameTitle)).build())
52-
.collectList()
53-
.flatMapMany(games -> {
54-
log.info("Games for `{}`: {}", gameTitle, games);
55-
return Flux.fromStream(games.stream());
56-
})
57-
.flatMap(game -> kumiteServer.searchContests(
58-
ContestSearchParameters.builder().gameId(Optional.of(game.getGameId())).build()))
59-
.flatMap(contest -> kumiteServer.loadBoard(playerId, contest.getContestId()))
60-
.filter(c -> !c.getDynamicMetadata().isGameOver())
61-
.filter(c -> c.getDynamicMetadata().isAcceptingPlayers())
62-
.doOnNext(contestView -> {
63-
UUID contestId = contestView.getContestId();
64-
65-
if (contestView.getPlayingPlayer().isPlayerHasJoined()) {
66-
log.info("Received board for already joined contestId={}", contestId);
67-
68-
playingContests.add(contestId);
69-
contestToDetails.put(contestId, contestView);
70-
} else if (contestView.getPlayingPlayer().isPlayerCanJoin()) {
71-
log.info("Received board for joinable contestId={}", contestId);
72-
kumiteServer.joinContest(playerId, contestId).subscribe(view -> {
73-
log.info("Received board for joined contestId={}", contestId);
74-
contestToDetails.put(contestId, contestView);
75-
});
76-
}
77-
})
78-
.subscribe(view -> {
79-
log.info("View: {}", view);
80-
});
81-
}, 1, 60, TimeUnit.SECONDS);
82-
});
44+
ses.scheduleWithFixedDelay(() -> {
45+
try {
46+
log.info("Playing contests as {}", playerId);
47+
kumitePlayer.playOptimizationGames(playerId);
48+
} catch (Throwable t) {
49+
log.warn("Issue while playing games", t);
50+
}
51+
52+
}, 1, 60, TimeUnit.SECONDS);
53+
54+
ses.scheduleWithFixedDelay(() -> {
55+
try {
56+
log.info("Playing contests as {}", playerId);
57+
kumitePlayer.play1v1(playerId);
58+
} catch (Throwable t) {
59+
log.warn("Issue while playing games", t);
60+
}
61+
62+
}, 1, 60, TimeUnit.SECONDS);
8363

8464
return null;
8565
}
8666

67+
8768
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
package eu.solven.kumite.app.player;
2+
3+
import java.util.UUID;
4+
5+
public interface IKumitePlayer {
6+
7+
void playOptimizationGames(UUID playerId);
8+
9+
void play1v1(UUID playerId);
10+
11+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,189 @@
1+
package eu.solven.kumite.app.player;
2+
3+
import java.time.Duration;
4+
import java.util.Map;
5+
import java.util.Optional;
6+
import java.util.UUID;
7+
8+
import eu.solven.kumite.app.server.IKumiteServer;
9+
import eu.solven.kumite.contest.ContestSearchParameters;
10+
import eu.solven.kumite.contest.ContestView;
11+
import eu.solven.kumite.game.GameSearchParameters;
12+
import eu.solven.kumite.game.IGameMetadataConstants;
13+
import eu.solven.kumite.player.PlayerRawMovesHolder;
14+
import lombok.AllArgsConstructor;
15+
import lombok.NonNull;
16+
import lombok.extern.slf4j.Slf4j;
17+
import reactor.core.publisher.Mono;
18+
19+
@AllArgsConstructor
20+
@Slf4j
21+
public class KumitePlayer implements IKumitePlayer {
22+
final IKumiteServer kumiteServer;
23+
24+
/**
25+
* Optimization games are the simplest one in term of integration: one just have to publish one solution to get on
26+
* the leaderboard
27+
*
28+
* @param kumiteServer
29+
* @param playerId
30+
*/
31+
@Override
32+
public void playOptimizationGames(UUID playerId) {
33+
GameSearchParameters optimizationsGameSearch =
34+
GameSearchParameters.builder().requiredTag(IGameMetadataConstants.TAG_OPTIMIZATION).build();
35+
36+
// Given a list of human-friendly game title
37+
// Flux.fromStream(Stream.of("Travelling Salesman Problem", "Tic-Tac-Toe"))
38+
// Build a SearchGames query
39+
// .map(gameTitle -> ))
40+
Mono.just(optimizationsGameSearch)
41+
// Search the games
42+
.flatMapMany(gameSearch -> {
43+
log.info("Looking for games matching `{}`", gameSearch);
44+
return kumiteServer.searchGames(gameSearch);
45+
})
46+
// Search for contests for given game
47+
.flatMap(game -> kumiteServer.searchContests(
48+
ContestSearchParameters.builder().gameId(Optional.of(game.getGameId())).build()))
49+
// Load the board for given contest
50+
.flatMap(contest -> kumiteServer.loadBoard(playerId, contest.getContestId()))
51+
// Filter interesting boards
52+
.filter(c -> !c.getDynamicMetadata().isGameOver())
53+
.filter(c -> c.getDynamicMetadata().isAcceptingPlayers())
54+
// Process each contest
55+
.flatMap(contestView -> {
56+
UUID contestId = contestView.getContestId();
57+
58+
if (contestView.getPlayingPlayer().isPlayerHasJoined()) {
59+
log.info("Received board for already joined contestId={}", contestId);
60+
return Mono.empty();
61+
} else if (contestView.getPlayingPlayer().isPlayerCanJoin()) {
62+
log.info("Received board for joinable contestId={}", contestId);
63+
return kumiteServer.joinContest(playerId, contestId)
64+
// We load the board again once we are signed-up
65+
.flatMap(playingPlayer -> kumiteServer.loadBoard(contestId, playerId));
66+
} else {
67+
log.info("We can not join contest={}", contestId);
68+
return Mono.empty();
69+
}
70+
})
71+
.flatMap(joinedContestView -> {
72+
UUID contestId = joinedContestView.getContestId();
73+
74+
if (joinedContestView.getDynamicMetadata().isGameOver()) {
75+
log.info("contestId={} is gameOver", contestId);
76+
return Mono.empty();
77+
}
78+
79+
Mono<PlayerRawMovesHolder> exampleMoves =
80+
kumiteServer.getExampleMoves(joinedContestView.getPlayingPlayer().getPlayerId(), contestId);
81+
82+
return exampleMoves.flatMap(moves -> {
83+
Optional<Map<String, ?>> selectedMove = selectMove(joinedContestView.getBoard(), moves);
84+
85+
if (selectedMove.isEmpty()) {
86+
log.info("No move. We quit the game. contestId=", contestId);
87+
return Mono.empty();
88+
}
89+
90+
return kumiteServer.playMove(playerId, joinedContestView.getContestId(), selectedMove.get());
91+
});
92+
})
93+
.flatMap(contestView -> {
94+
return kumiteServer.loadLeaderboard(contestView.getContestId()).doOnNext(leaderboard -> {
95+
log.info("contestid={} leaderbord={}", contestView.getContestId(), leaderboard);
96+
});
97+
})
98+
.subscribe();
99+
}
100+
101+
/**
102+
* 1v1 games needs sone sort of `do-while` loop, to play moves until the game is over.
103+
*
104+
* @param kumiteServer
105+
* @param playerId
106+
*/
107+
@Override
108+
public void play1v1(UUID playerId) {
109+
GameSearchParameters optimizationsGameSearch =
110+
GameSearchParameters.builder().requiredTag(IGameMetadataConstants.TAG_1V1).build();
111+
112+
Mono.just(optimizationsGameSearch)
113+
// Search the games
114+
.flatMapMany(gameSearch -> {
115+
log.info("Looking for games matching `{}`", gameSearch);
116+
return kumiteServer.searchGames(gameSearch);
117+
})
118+
// Search for contests for given game
119+
.flatMap(game -> kumiteServer.searchContests(
120+
ContestSearchParameters.builder().gameId(Optional.of(game.getGameId())).build()))
121+
// Load the board for given contest
122+
.flatMap(contest -> kumiteServer.loadBoard(playerId, contest.getContestId()))
123+
// Filter interesting boards
124+
.filter(c -> !c.getDynamicMetadata().isGameOver())
125+
.filter(c -> c.getDynamicMetadata().isAcceptingPlayers())
126+
// Process each contest
127+
.flatMap(contestView -> {
128+
UUID contestId = contestView.getContestId();
129+
130+
if (contestView.getPlayingPlayer().isPlayerHasJoined()) {
131+
log.info("Received board for already joined contestId={}", contestId);
132+
return Mono.empty();
133+
} else if (contestView.getPlayingPlayer().isPlayerCanJoin()) {
134+
log.info("Received board for joinable contestId={}", contestId);
135+
return kumiteServer.joinContest(playerId, contestId)
136+
.flatMap(playingPlayer -> kumiteServer.loadBoard(contestId, playerId));
137+
} else {
138+
log.info("We can not join contest={}", contestId);
139+
return Mono.empty();
140+
}
141+
})
142+
// This acts like a `do-while` loop: we loop by playing moves until the game is over
143+
// https://codersee.com/project-reactor-expand/
144+
.expand(joinedContestView -> {
145+
UUID contestId = joinedContestView.getContestId();
146+
147+
if (joinedContestView.getDynamicMetadata().isGameOver()) {
148+
log.info("contestId={} is gameOver", contestId);
149+
return Mono.empty();
150+
}
151+
152+
Mono<PlayerRawMovesHolder> exampleMoves =
153+
kumiteServer.getExampleMoves(joinedContestView.getPlayingPlayer().getPlayerId(), contestId);
154+
Mono<ContestView> monoContestViewPostMove = exampleMoves.flatMap(moves -> {
155+
Optional<Map<String, ?>> optSelectedMove = selectMove(joinedContestView.getBoard(), moves);
156+
157+
if (optSelectedMove.isEmpty()) {
158+
// There is no available move: wait until gameOver
159+
Duration delay = Duration.ofSeconds(5);
160+
log.info("No move. We wait {} for {}", delay, contestId);
161+
return Mono.just(joinedContestView).delayElement(delay);
162+
}
163+
164+
Map<String, ?> selectedMove = optSelectedMove.get();
165+
log.info("We playMove `{}`for {}", selectedMove);
166+
return kumiteServer.playMove(playerId, joinedContestView.getContestId(), selectedMove);
167+
});
168+
return monoContestViewPostMove;
169+
})
170+
.flatMap(contestView -> {
171+
return kumiteServer.loadLeaderboard(contestView.getContestId()).doOnNext(leaderboard -> {
172+
log.info("contestid={} leaderbord={}", contestView.getContestId(), leaderboard);
173+
});
174+
})
175+
.subscribe();
176+
}
177+
178+
/**
179+
* Select one move amongst the examples moves.
180+
*
181+
* @param board
182+
* @param moves
183+
* @return the first suggested move.
184+
*/
185+
private Optional<Map<String, ?>> selectMove(@NonNull Map<String, ?> board, PlayerRawMovesHolder moves) {
186+
// WaitForPlayersMove and WaitForSignups would have a `wait:true` flag
187+
return moves.getMoves().values().stream().filter(m -> !Boolean.TRUE.equals(m.get("wait"))).findAny();
188+
}
189+
}

player/src/main/java/eu/solven/kumite/app/IKumiteServer.java player/src/main/java/eu/solven/kumite/app/server/IKumiteServer.java

+6-2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
package eu.solven.kumite.app;
1+
package eu.solven.kumite.app.server;
22

33
import java.util.Map;
44
import java.util.UUID;
@@ -8,6 +8,7 @@
88
import eu.solven.kumite.contest.ContestView;
99
import eu.solven.kumite.game.GameMetadata;
1010
import eu.solven.kumite.game.GameSearchParameters;
11+
import eu.solven.kumite.leaderboard.LeaderBoardRaw;
1112
import eu.solven.kumite.player.PlayerRawMovesHolder;
1213
import eu.solven.kumite.player.PlayingPlayer;
1314
import reactor.core.publisher.Flux;
@@ -18,12 +19,15 @@ public interface IKumiteServer {
1819

1920
Flux<ContestMetadataRaw> searchContests(ContestSearchParameters contestSearchParameters);
2021

21-
Mono<ContestView> loadBoard(UUID contestId, UUID playerId);
22+
Mono<ContestView> loadBoard(UUID playerId, UUID contestId);
2223

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

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

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

31+
Mono<LeaderBoardRaw> loadLeaderboard(UUID contestId);
32+
2933
}

0 commit comments

Comments
 (0)