center of our universe
zosma
is a reactive game agent modeling framework. Its purpose is to
provide a minimal declarative framework with which to process game data.
You'll need at least Java 1.8 (jre
/jdk)
to run zosma
.
<dependency>
<groupId>io.leonis</groupId>
<artifactId>zosma</artifactId>
<version>0.0.8</version>
</dependency>
compile 'io.leonis:zosma:0.0.8'
The first step in creating a game agent is to form some notion of game state, and how it changes over time. A game state is used by the game agent in order to compute possible ways to play. Usually game data is broadcast by a game host server (or some other application) which measures (or simulates) the state of a game. For instance, one could represent the state of a chess game as follows (getters ommitted for brevity):
@Value
public class ChessGame {
private final long timestamp;
private final Map<ChessPiece, Location> chessPiecePositions;
private final Player currentPlayer;
public enum Player {
WHITE,
BLACK
}
}
In a similar fashion game agents must have some notion of strategic state, and how it changes over time, in order to decide the exact way in which play must be executed. Take for instance the following representation of a chess strategy:
@Value
public class ChessStrategy {
private final long timestamp;
private final ChessPiece toMove;
private final Location destination;
}
Ultimately a game agent is nothing more than logic which allow consecutive frames of game data to
to be translated into game strategies. In Java this could be expressed as a function which takes
a Publisher
of game state representations and returns a Publisher
of strategy representations, or
Function<Publisher<GameState>, Publisher<Strategy>>
. These strategies can then be relayed to a
system which executes them. Note that trying to implement such a system using
Function<GameState, Strategy>
only will not allow the implementation to take into account
previously received game state representations, or previously emitted strategy representations,
whereas a Function<Publisher<GameState>, Publisher<Strategy>>
will:
// create a stream of game state representations
Flux.from(chessGamePublisher)
.transform(chessGames ->
Flux.from(chessGames)
// store the two most recent game states in a List
.buffer(2, 1)
.map(previousTwoGames -> {
// ... do something with the two most recent game state representations
}));
A game (usually) contains some objects which can interact with each other. These interactions mutate
state of the game. In the example below the ChessPiece
is a data representation of such an
object. zosma
requires that each of these objects be identifiable, i.e. it should be able to
supply an identity (getters omitted for brevity):
@Value
public class ChessPiece implements Identity.Supplier {
private final Location location;
private final ChessPieceIdentity identity;
@Value
public static class ChessPieceIdentity implements Identity {
private final ChessPiece.Type type;
private final ChessGame.Player owner;
}
public enum Type {
KING,
QUEEN,
ROOK,
BISHOP,
KNIGHT,
PAWN
}
}
A strategy will often specify some desired configuration of agents. This configuration can be purely positional, but can also be used for assigning roles to agents (not applicable to chess, since chess roles are not dynamic). For example:
@Value
public class ChessFormation implements Formation<ChessPiece, Location> {
private final Map<ChessPiece, Location> desiredFormation;
@Override
public Location getFormationFor(final ChessPiece piece) {
return this.desiredFormation.get(piece);
}
}
The legality of an action in a game and the win-conditions are determined by the rules of the game. The game agent must know these rules in order to determine the validity of its own potential moves or to anticipate a specific state of the game. A rule is evaluated by examining a game state:
public class LoseCondition implements Rule<ChessGame, ChessGame.Player> {
@Override
Set<ChessGame.Player> getViolators(final ChessGame input) {
// a player has lost, if
return input.getChessPiecePositions().keySet().stream()
// its king
.filter(chessPiece -> chessPiece.getIdentity().getType().equals(ChessPiece.Type.KING))
// is off the board
.filter(chessPiece -> chessPiece.getLocation.equals(Location.OUTSIDE)
.map(chessPiece -> chessPiece.getIdentity().getOwner())
.collect(Collectors.toSet()));
}
}
Incoming data is not always expressed in a compact form such as the snippets described in the system
overview. When creating game agents using pre-existing protocols, or external systems, the provided
data representations are often bloated or contain data in undesirable formats. Similarly, data which
has been received may need to be embellished with information which can be used downstream.
This pattern of transforming emitted data is so prevalent that it has been explicitly typed in
zosma
under the name Deducer
. Note that Deducer
is simply an interface
for an Rx operator:
public class ChessStrategyDeducer implements Deducer<ChessGame, ChessStrategy> {
@Override
public Publisher<ChessStrategy> apply(final Publisher<ChessGame> chessGamePublisher) {
// compute strategy
}
}
Which would then be used as follows:
Flux.from(chessGamePublisher)
.transform(new ChessFormationDeducer())
N.B.: There is a IdentityDeducer
for when the input-type is the same as the output-type. Examples
of use cases where this may occur include application of filters and other correction mechanism.
At any point during data processing it might be desirable to parallelize certain computations and
combine their results. zosma
contains a ParallelDeducer
which allow the most recent emissions
of a list of deducers to be combined:
Flux.from(chessGamePublisher)
.transform(
new ParallelDeducer<>(
new ChessStrategyDeducer(),
new OtherChessStrategyDeducer(),
(chessStrategy, otherChessStrategy) -> {
// combine, pick, or create a new strategy and return it
}))
As can be seen in the example above, ParallelDeducer
takes deducers as its first n arguments
(n <= 10) and a combining function as its last argument. The combining function takes n arguments,
where each argument corresponds to the latest emission by the nth provided deducer.
zosma
provides a helper class named Zosma
for modeling game IO in five parts which represents the
run configuration of the system:
- Publisher of external input
- The external input to the system, which is represented as a
Publisher
of the raw input to the system.
- The external input to the system, which is represented as a
- Internal input adapter (optional)
- The internal input adapter to the system, which transforms data emitted by the external input into a format which can be processed by the game agent.
- Internal output adapter
- The internal output adapter to the system, which transforms data emitted by the internal input adapter into a format which can be processed by the internal output adapter. This is where the game logic resides.
- External output adapter (optional)
- The external output adapter to the system, which transforms data emitted by the internal output adapter into a format which can be processed by the external output adapter.
- Subscriber
- The subscription mechanism of the system, which consumes data emitted by the external output adapter.
The internal input adapter and external output adapter are optional adapters which allow data types from external libraries to be converted to an intermediary type which is interpreted by the game agent.
Following the example of a chess game, Zosma
might be instantiated in the long form as follows:
final Zosma chessZosma = new Zosma<>(
new ChessGamePublisher(),
new ChessGameRepresentationAdapter(),
new ChessStrategizer(),
new ChessStrategyRepresentationAdapter(),
new ChessStrategySubscriber());
Or in the short form as follows:
final Zosma chessZosma = new Zosma<>(
new ChessGamePublisher(),
new ChessStrategizer(),
new ChessStrategySubscriber());
Once instantiated, the game agent can be run by calling Zosma#run
:
chessZosma.run();
Notice that Zosma
implements the Runnable
interface which means you can run it in a separate thread
or make use of ExecutorService
in order to manage the life cycle of the game agent:
final ExecutorService zosmaExecutor = Executors.newSingleThreadExecutor();
final Future futureZosma = zosmaExecutor.submit(chessZosma);
Since the data evolves as it is processed the type of the data container may change as well. In
order to allow composition of these data containers through Deducer
s, these Deducer
s should
be parametrized using interface instead of class types. Unfortunately Java's Supplier
can only be implemented once due to type erasure; this is why most data types in zosma
have a
nested Supplier
(or SetSupplier
, MappingSupplier
, e.a.) interface (see Rule
or Controller for example).
This allows for both of the following classes:
@Value
public class InputType implements Controller.MappingSupplier, Rule.SetSupplier {
private final Map<Controller, Set<Agent>> agentMapping;
private final Set<Rule> rules;
}
@Value
public class AnotherInputType implements Rule.SetSupplier {
private final Set<Rule> rules;
}
To be acted on by the following Deducer
:
public class ExampleDeducer<I implements Rule.SetSupplier> implements IdentityDeducer<I> { /** ... */ }
Notice that Java allows multiple bounds on type parameters, so it is possible to require multiple suppliers on the generic input type as follows:
public class AnotherExampleDeducer<I implements Rule.SetSupplier & Controller.MappingSupplier>
implements IdentityDeducer<I>{ /** ... */ }
zosma
contains two helper packages, ipc
and function
. The ipc
package contains Publisher
s
and Subscriber
s for common protocols (such as TCP, UDP, multicast, and others). The function
package contains functional interfaces for Function3
to Function10
which are missing from the
JDK.
The javadoc for the current code on master
can be found on https://delta-leonis.github.io/zosma/
Make sure you have gradle>=v2.10
installed. Run the following to build the application:
gradle build
This project is licensed under the AGPL version 3 license (see LICENSE).
zosma - delta-leonis
Copyright (C) 2017 Rimon Oz, Jeroen de Jong, Ryan Meulenkamp, Thomas Hakkers
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.