Skip to content

Game Demo Port #611

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 17 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
**/Cargo.lock

.idea
.vscode

# Don't ignore Cargo.lock files in demo applications
!/demos/**/Cargo.lock
Expand Down
6 changes: 4 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,8 @@ members = [
"example_apps/ripple",
"example_apps/stocks_simulated",
"example_apps/kafka_ingress_connector",
"example_apps/kafka_egress_connector"
"example_apps/kafka_egress_connector",
"example_apps/game"
]

[workspace.package]
Expand Down Expand Up @@ -182,4 +183,5 @@ http-body-util = "0.1.2"
hyper-util = "0.1.5"
rdkafka = "0.36"
apache-avro = "0.17.0"
time = "0.3.36"
time = "0.3.36"
rand_distr = "0.4.3"
20 changes: 20 additions & 0 deletions example_apps/game/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
[package]
name = "game"
version = "0.1.0"
edition = "2021"

[dependencies]
swimos = { path = "../../swimos", features = ["server", "agent"] }
swimos_form = { path = "../../api/swimos_form" }
swimos_utilities = { workspace = true, features = ["trigger"] }
tokio = { workspace = true, features = ["rt-multi-thread", "macros", "signal", "fs"]}
futures = { workspace = true }
example-util = { path = "../example_util" }
tracing = { workspace = true }
tracing-subscriber = { workspace = true, features = ["env-filter"] }
clap = { workspace = true, features = ["derive"]}
tokio-stream = { workspace = true, features = ["io-util"]}
tokio-util = { workspace = true, features = ["io"]}
rand = { workspace = true }
rand_distr = { workspace = true }
axum = { workspace = true, features = ["tokio", "http1"] }
30 changes: 30 additions & 0 deletions example_apps/game/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
Game Leaderboard Application
===================

Swim server that simultes a game universe and matches between players. Results of matches are aggregated and a real-time global leaderboard agent is maintained.

This is a direct port of the Java Swim game application that can be found [here](https://github.com/swimos/demos).

Running
-------

The application can be run (from the root directory of the game project) with:

```
WS_NO_SUBPROTOCOL_CHECK=true cargo run --bin game -- --port 9001 --include-ui --ui-port 9002
```

The swim server will run on the port specified with `--port` and the web UI will be available at the port specified with `--ui-port`. Either of these can be omitted which will cause them to bind to any available port.

The web UI can then be found at:

```
http://127.0.0.1:9002/index.html?host=ws://localhost:9001
```

Logging
-------

The application has a default logging configuration which can be applied by passing `--enable-logging` on the command line. This will log directly to the console.

The default configuration may be altered using the standard `RUST_LOG` environment variable.
115 changes: 115 additions & 0 deletions example_apps/game/src/agents/battle.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
// Copyright 2015-2024 Swim Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

use crate::generator::game::Game;
use swimos::agent::{
agent_lifecycle::HandlerContext,
event_handler::{EventHandler, HandlerActionExt, Sequentially},
lanes::{CommandLane, ValueLane},
lifecycle, projections, AgentLaneModel,
};
use tracing::info;

use super::model::stats::MatchSummary;

#[derive(AgentLaneModel)]
#[projections]
#[agent(transient, convention = "camel")]
pub struct MatchAgent {
// Summary of the match
stats: ValueLane<Option<MatchSummary>>,
// Publish the match
publish: CommandLane<Game>,
}

#[derive(Clone)]
pub struct MatchLifecycle;

#[lifecycle(MatchAgent)]
impl MatchLifecycle {
#[on_command(publish)]
fn publish_match(
&self,
context: HandlerContext<MatchAgent>,
game: &Game,
) -> impl EventHandler<MatchAgent> {
let game = game.clone();

context
.effect(move || {
info!(id = game.id, "New match published.");
game
})
.map(Game::into)
.and_then(move |summary: MatchSummary| {
context
.set_value(MatchAgent::STATS, Some(summary.clone()))
.followed_by(forward_match_summary(context, summary))
})
}

#[on_start]
fn starting(&self, context: HandlerContext<MatchAgent>) -> impl EventHandler<MatchAgent> {
context
.get_agent_uri()
.and_then(move |uri| context.effect(move || info!(uri = %uri, "Starting match agent")))
}

#[on_stop]
fn stopping(&self, context: HandlerContext<MatchAgent>) -> impl EventHandler<MatchAgent> {
context
.get_agent_uri()
.and_then(move |uri| context.effect(move || info!(uri = %uri, "Stopping match agent")))
}
}

fn forward_match_summary(
context: HandlerContext<MatchAgent>,
match_summary: MatchSummary,
) -> impl EventHandler<MatchAgent> {
let forward_to_players = match_summary
.player_stats
.keys()
.map(|id| {
command_match_summary(
context,
match_summary.clone(),
format!("/player/{id}").as_str(),
)
})
.collect::<Vec<_>>();
let forward_to_teams = match_summary
.team_stats
.keys()
.map(|name| {
command_match_summary(
context,
match_summary.clone(),
format!("/team/{name}").as_str(),
)
})
.collect::<Vec<_>>();
let forward_to_game = command_match_summary(context, match_summary, "/match");
forward_to_game
.followed_by(Sequentially::new(forward_to_players))
.followed_by(Sequentially::new(forward_to_teams))
}

fn command_match_summary(
context: HandlerContext<MatchAgent>,
match_summary: MatchSummary,
node_uri: &str,
) -> impl EventHandler<MatchAgent> {
context.send_command(None, node_uri, "addMatch", match_summary)
}
127 changes: 127 additions & 0 deletions example_apps/game/src/agents/game.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
// Copyright 2015-2024 Swim Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

use std::{cell::RefCell, collections::VecDeque, time::Duration};

use crate::generator::universe::Universe;
use swimos::agent::{
agent_lifecycle::HandlerContext,
event_handler::{EventHandler, HandlerActionExt},
lanes::{CommandLane, MapLane, ValueLane},
lifecycle, projections, AgentLaneModel,
};
use tracing::info;

use super::model::stats::{MatchSummary, MatchTotals};

#[derive(AgentLaneModel)]
#[projections]
#[agent(transient, convention = "camel")]
pub struct GameAgent {
// Total stats across all matches
stats: ValueLane<MatchTotals>,
// History of all matches
match_history: MapLane<u64, MatchSummary>,
// Add a match to the totals and history
add_match: CommandLane<MatchSummary>,
}

#[derive(Debug, Clone, Default)]
pub struct GameLifecycle {
timestamps: RefCell<VecDeque<u64>>,
}

impl GameLifecycle {
fn update_timestamps(&self, timestamp: u64) -> impl EventHandler<GameAgent> + '_ {
let context: HandlerContext<GameAgent> = Default::default();
context
.effect(move || {
let mut guard = self.timestamps.borrow_mut();
guard.push_back(timestamp);
if guard.len() > 250 {
guard.pop_front()
} else {
None
}
})
.and_then(move |to_remove: Option<u64>| to_remove.map(remove_old).discard())
}
}

fn remove_old(to_remove: u64) -> impl EventHandler<GameAgent> {
let context: HandlerContext<GameAgent> = Default::default();
context.remove(GameAgent::MATCH_HISTORY, to_remove)
}

#[lifecycle(GameAgent)]
impl GameLifecycle {
#[on_start]
fn starting(&self, context: HandlerContext<GameAgent>) -> impl EventHandler<GameAgent> + '_ {
let mut universe = Universe::default();

context
.get_agent_uri()
.and_then(move |uri| {
context.effect(move || info!(uri = %uri, "Starting game agent, generating matches"))
})
.followed_by(
context.schedule_repeatedly(Duration::from_secs(3), move || {
Some(generate_match(&mut universe, context))
}),
)
}

#[on_stop]
fn stopping(&self, context: HandlerContext<GameAgent>) -> impl EventHandler<GameAgent> {
context
.get_agent_uri()
.and_then(move |uri| context.effect(move || info!(uri = %uri, "Stopping game agent")))
}

#[on_command(add_match)]
fn add_match(
&self,
context: HandlerContext<GameAgent>,
match_summary: &MatchSummary,
) -> impl EventHandler<GameAgent> + '_ {
let summary: MatchSummary = match_summary.clone();
let ts = summary.start_time;
let summary_history = match_summary.clone();
context
.get_value(GameAgent::STATS)
.and_then(move |mut current: MatchTotals| {
current.increment(&summary);
context.set_value(GameAgent::STATS, current)
})
.followed_by(context.update(
GameAgent::MATCH_HISTORY,
summary_history.start_time,
summary_history,
))
.followed_by(self.update_timestamps(ts))
}
}

fn generate_match(
universe: &mut Universe,
context: HandlerContext<GameAgent>,
) -> impl EventHandler<GameAgent> {
let game = universe.generate_game();
context.send_command(
None,
format!("/match/{id}", id = game.id).as_str(),
"publish",
game,
)
}
Loading
Loading