Show players in lobby

This commit is contained in:
ajuvercr 2020-04-01 20:49:50 +02:00
parent c64535675f
commit 86ffa9726c
14 changed files with 325 additions and 126 deletions

View file

@ -3,6 +3,7 @@
extern crate serde; extern crate serde;
#[macro_use] #[macro_use]
extern crate serde_derive; extern crate serde_derive;
#[macro_use]
extern crate serde_json; extern crate serde_json;
extern crate async_std; extern crate async_std;
@ -76,7 +77,7 @@ async fn main() {
let pool = ThreadPool::new().unwrap(); let pool = ThreadPool::new().unwrap();
pool.spawn_ok(fut.map(|_| ())); pool.spawn_ok(fut.map(|_| ()));
let gm = create_game_manager("0.0.0.0:9142", pool.clone()); let gm = create_game_manager("0.0.0.0:9142", pool.clone()).await;
let mut routes = Vec::new(); let mut routes = Vec::new();
@ -98,12 +99,12 @@ async fn main() {
.unwrap(); .unwrap();
} }
fn create_game_manager(tcp: &str, pool: ThreadPool) -> game::Manager { async fn create_game_manager(tcp: &str, pool: ThreadPool) -> game::Manager {
let addr = tcp.parse::<SocketAddr>().unwrap(); let addr = tcp.parse::<SocketAddr>().unwrap();
let (gmb, handle) = game::Manager::builder(pool.clone()); let (gmb, handle) = game::Manager::builder(pool.clone());
pool.spawn_ok(handle.map(|_| ())); pool.spawn_ok(handle.map(|_| ()));
let ep = TcpEndpoint::new(addr, pool.clone()); let ep = TcpEndpoint::new(addr, pool.clone());
let gmb = gmb.add_endpoint(ep, "TCP endpoint"); let gmb = gmb.add_endpoint(ep, "TCP endpoint");
gmb.build() gmb.build("games.ini", pool).await.unwrap()
} }

View file

@ -23,7 +23,6 @@ pub struct PlanetWarsGame {
log_file: File, log_file: File,
turns: u64, turns: u64,
name: String, name: String,
logged: bool,
} }
impl PlanetWarsGame { impl PlanetWarsGame {
@ -47,7 +46,6 @@ impl PlanetWarsGame {
log_file: file, log_file: file,
turns: 0, turns: 0,
name: name.to_string(), name: name.to_string(),
logged: false,
} }
} }
@ -169,8 +167,8 @@ impl PlanetWarsGame {
} }
} }
use ini::Ini; use serde_json::Value;
use std::fs::OpenOptions;
impl game::Controller for PlanetWarsGame { impl game::Controller for PlanetWarsGame {
fn start(&mut self) -> Vec<HostMsg> { fn start(&mut self) -> Vec<HostMsg> {
let mut updates = Vec::new(); let mut updates = Vec::new();
@ -194,31 +192,25 @@ impl game::Controller for PlanetWarsGame {
updates updates
} }
fn is_done(&mut self) -> bool { fn is_done(&mut self) -> Option<Value> {
if self.state.is_finished() { if self.state.is_finished() {
if !self.logged { Some(json!({
let mut f = match OpenOptions::new() "winners": self.state.living_players(),
.create(true) "turns": self.state.turn_num,
.append(true) "name": self.name.clone(),
.open("games.ini") "file": self.log_file_loc.clone(),
{ }))
Err(_) => return true,
Ok(f) => f,
};
let mut conf = Ini::new();
conf.with_section(Some(self.log_file_loc.clone()))
.set("name", &self.name)
.set("turns", format!("{}", self.turns));
conf.write_to(&mut f).unwrap();
self.logged = true;
}
true
} else { } else {
false None
} }
} }
} }
#[derive(Debug, Serialize, Deserialize)]
pub struct FinishedState {
pub winners: Vec<u64>,
pub turns: u64,
pub name: String,
pub file: String,
pub players: Vec<(u64, String)>,
}

View file

@ -116,17 +116,23 @@ async fn game_post(game_req: Json<GameReq>, tp: State<'_, ThreadPool>, gm: State
let game_id = gm.start_game(game).await.unwrap(); let game_id = gm.start_game(game).await.unwrap();
state.add_game(game_req.name.clone(), game_id); state.add_game(game_req.name.clone(), game_id);
if let Some(conns) = gm.get_state(game_id).await { match gm.get_state(game_id).await {
Some(Ok(conns)) => {
let players: Vec<u64> = conns.iter().map(|conn| match conn { let players: Vec<u64> = conns.iter().map(|conn| match conn {
Connect::Waiting(_, key) => *key, Connect::Waiting(_, key) => *key,
_ => 0, _ => 0,
}).collect(); }).collect();
Ok(Json(GameRes { players })) Ok(Json(GameRes { players }))
} else { },
Some(Err(v)) => {
Err(serde_json::to_string(&v).unwrap())
},
None => {
Err(String::from("Fuck the world")) Err(String::from("Fuck the world"))
} }
} }
}
pub fn fuel(routes: &mut Vec<Route>) { pub fn fuel(routes: &mut Vec<Route>) {
routes.extend(routes![files, index, map_post, map_get, lobby_get, builder_get, visualizer_get, game_post, state_get, debug_get]); routes.extend(routes![files, index, map_post, map_get, lobby_get, builder_get, visualizer_get, game_post, state_get, debug_get]);

View file

@ -9,12 +9,67 @@ pub struct Map {
url: String, url: String,
} }
#[derive(Serialize)] #[derive(Serialize, Eq, PartialEq)]
pub struct GameState { pub struct PlayerStatus {
waiting: bool,
connected: bool,
reconnecting: bool,
value: String,
}
impl From<Connect> for PlayerStatus {
fn from(value: Connect) -> Self {
match value {
Connect::Connected(_, name) => PlayerStatus { waiting: false, connected: true, reconnecting: false, value: name },
Connect::Reconnecting(_, name) => PlayerStatus { waiting: false, connected: true, reconnecting: true, value: name },
Connect::Waiting(_, key) => PlayerStatus { waiting: true, connected: false, reconnecting: false, value: format!("Key: {}", key) },
_ => panic!("No playerstatus possible from Connect::Request"),
}
}
}
#[derive(Serialize, Eq, PartialEq)]
#[serde(tag = "type")]
pub enum GameState {
Finished {
name: String, name: String,
finished: bool, map: String,
turns: Option<u64>, players: Vec<(String, bool)>,
players: Vec<String>, turns: u64,
},
Playing {
name: String,
map: String,
players: Vec<PlayerStatus>,
connected: usize,
total: usize,
}
}
use std::cmp::Ordering;
impl PartialOrd for GameState {
fn partial_cmp(&self, other: &GameState) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl Ord for GameState {
fn cmp(&self, other: &GameState) -> Ordering {
match self {
GameState::Finished { name, .. } => {
match other {
GameState::Finished { name: _name, .. } => name.cmp(_name),
_ => Ordering::Greater,
}
},
GameState::Playing { name, .. } => {
match other {
GameState::Playing { name: _name, .. } => name.cmp(_name),
_ => Ordering::Less,
}
}
}
}
} }
/// Visualiser game option /// Visualiser game option
@ -120,6 +175,7 @@ pub async fn get_games() -> Result<Vec<GameOption>, String> {
Ok(games) Ok(games)
} }
use crate::planetwars::FinishedState;
use mozaic::modules::game; use mozaic::modules::game;
use mozaic::util::request::Connect; use mozaic::util::request::Connect;
@ -130,34 +186,33 @@ pub async fn get_states(game_ids: &Vec<(String, u64)>, manager: &game::Manager)
for (gs, name) in gss { for (gs, name) in gss {
if let Some(state) = gs { if let Some(state) = gs {
let mut players: Vec<String> = state.iter().map(|conn| match conn { match state {
Connect::Waiting(_, key) => format!("Waiting {}", key), Ok(conns) => {
_ => String::from("Some connected player") let players: Vec<PlayerStatus> = conns.iter().cloned().map(|x| x.into()).collect();
}).collect(); let connected = players.iter().filter(|x| x.connected).count();
players.sort();
states.push( states.push(
GameState { GameState::Playing { name: name, total: players.len(), players, connected, map: String::new(), }
name, );
turns: None, },
players: players, Err(value) => {
finished: false, let state: FinishedState = serde_json::from_value(value).expect("Shit failed");
}
)
} else {
states.push( states.push(
GameState { GameState::Finished {
name, map: String::new(),
turns: None, players: state.players.iter().map(|(id, name)| (name.clone(), state.winners.contains(&id))).collect(),
players: Vec::new(), name: state.name,
finished: true, turns: state.turns,
}
);
}
} }
)
} }
} }
states.sort_by_key(|a| a.name.clone()); states.sort();
println!(
"{}", serde_json::to_string_pretty(&states).unwrap(),
);
Ok(states) Ok(states)
} }

View file

@ -49,7 +49,7 @@ body {
height: 100%; height: 100%;
width: 100%; width: 100%;
display: flex; display: flex;
justify-content: center; justify-content: start;
background: black; background: black;
color: #ff7f00; color: #ff7f00;
line-height: 1.5rem; line-height: 1.5rem;
@ -66,11 +66,9 @@ body {
.info a { .info a {
/* color: inherit; */ /* color: inherit; */
/* filter: saturate(100%); */ /* filter: saturate(100%); */
/* filter: invert(75%); */ /* filter: invert(75%); */
color: #ff7f00; color: #ff7f00;
filter: brightness(0.5); filter: brightness(0.5);
font-weight: bold; font-weight: bold;
text-decoration: none; text-decoration: none;
} }
@ -88,35 +86,29 @@ body {
color: #ff7f00; color: #ff7f00;
margin-bottom: 20px; margin-bottom: 20px;
} }
.info div p { .info div p {
/* color: #ff7f00; */ /* color: #ff7f00; */
margin: 5px 0; margin: 5px 0;
} }
@keyframes heartbeat @keyframes heartbeat {
{ 0% {
0%
{
transform: rotate(-45deg) scale( .75); transform: rotate(-45deg) scale( .75);
} }
20% 20% {
{
transform: rotate(-45deg) scale( 1); transform: rotate(-45deg) scale( 1);
} }
40% 40% {
{
transform: rotate(-45deg) scale( .75); transform: rotate(-45deg) scale( .75);
} }
60% 60% {
{
transform: rotate(-45deg) scale( 1); transform: rotate(-45deg) scale( 1);
} }
80% 80% {
{
transform: rotate(-45deg) scale( .75); transform: rotate(-45deg) scale( .75);
} }
100% 100% {
{
transform: rotate(-45deg) scale( .75); transform: rotate(-45deg) scale( .75);
} }
} }
@ -130,9 +122,7 @@ body {
top: 0; top: 0;
transform: rotate(-45deg); transform: rotate(-45deg);
width: 20px; width: 20px;
filter: none; filter: none;
animation: heartbeat 2s infinite; animation: heartbeat 2s infinite;
} }

View file

@ -0,0 +1,76 @@
/*
CSS for the main interaction
*/
.accordion>input[type="checkbox"] {
position: absolute;
left: -100vw;
}
.accordion .content {
overflow-y: hidden;
height: 0;
transition: height 0.3s ease;
}
.accordion>input[type="checkbox"]:checked~.content {
height: auto;
overflow: visible;
}
.accordion label {
display: block;
}
/*
Styling
*/
.accordion {
margin: 1em;
width: 90%;
min-width: 250px;
max-width: 350px;
}
.accordion .content {
width: 100%;
}
.accordion>input[type="checkbox"]:checked~.content {
background-color: #555;
}
.accordion .handle {
margin: 0;
font-size: 1.125em;
line-height: 1.2em;
}
.accordion label {
color: #ff7f00;
cursor: pointer;
font-weight: normal;
padding: 15px;
background: #333;
}
.accordion label:hover,
.accordion label:focus {
filter: brightness(0.7);
}
.accordion .handle label:before {
font-family: 'fontawesome';
content: "\f054";
display: inline-block;
margin-right: 10px;
font-size: .58em;
line-height: 1.556em;
vertical-align: middle;
}
.accordion>input[type="checkbox"]:checked~.handle label:before {
content: "\f078";
}

View file

@ -15,8 +15,9 @@
} }
.grid { .grid {
width: 120vh; width: 85vh;
height: 95vh; height: 85vh;
margin: auto;
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
} }

View file

@ -0,0 +1,52 @@
.connected::before {
content: "";
display: block;
width: 16px;
height: 16px;
float: left;
margin: 0 -20px 0 0;
font-family: 'fontawesome';
content: "\f1eb";
transform: translate(-30px, 0px);
}
.waiting::before {
content: "";
display: block;
width: 16px;
height: 16px;
float: left;
margin: 0 -20px 0 0;
font-family: 'fontawesome';
content: "\f084";
transform: rotate(180deg) translate(27px, -7px) scaleX(-1);
}
.reconnecting::before {
content: "";
display: block;
width: 16px;
height: 16px;
float: left;
margin: 0 -20px 0 0;
font-family: 'fontawesome';
content: "\f0e7";
transform: translate(-22px, 0px);
}
.players {
margin: 10px 10px 10px 50px;
color: white;
}
.winner::before {
content: "";
display: block;
width: 16px;
height: 16px;
float: left;
margin: 0 -20px 0 0;
font-family: 'fontawesome';
content: "\f091";
transform: translate(-30px, 0px);
}

View file

@ -20,12 +20,12 @@ body {
right: 25px; right: 25px;
width: 40px; width: 40px;
height: 40px; height: 40px;
background-color: #ff7f00; background-color: #ff7f00;
mask-image: url("refresh.svg"); mask-image: url("refresh.svg");
-webkit-mask-image: url("refresh.svg"); -webkit-mask-image: url("refresh.svg");
animation: spin 2s linear infinite; animation: spin 2s linear infinite;
animation-play-state: paused; animation-play-state: paused;
z-index: 5;
} }
.refresh:hover { .refresh:hover {
@ -34,12 +34,15 @@ body {
} }
@keyframes spin { @keyframes spin {
100% {transform: rotate(1turn); } 100% {
transform: rotate(1turn);
}
} }
.lobby_wrapper { .lobby_wrapper {
flex-grow: 1; flex-grow: 1;
position: relative;; position: relative;
;
} }
.lobby { .lobby {
@ -48,7 +51,7 @@ body {
flex-wrap: wrap; flex-wrap: wrap;
align-self: flex-start; align-self: flex-start;
overflow-y: scroll; overflow-y: scroll;
height: 100%; justify-content: center;
} }
.input_container { .input_container {

View file

@ -1,11 +1,14 @@
{% extends "base" %} {% extends "base" %}
{% block content %} {% block content %}
<link rel="stylesheet" href="/style/collapsable.css">
<link rel="stylesheet" href="/style/state.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css">
<div class="main"> <div class="main">
<div class="lobby_wrapper"> <div class="lobby_wrapper">
<div class="refresh" onclick="refresh_state()"></div> <div class="refresh" onclick="refresh_state()"></div>
<div id="lobby" class="lobby"> <div id="lobby" class="lobby"></div>
</div>
</div> </div>
<div class="creator"> <div class="creator">
<h1>Start new game</h1> <h1>Start new game</h1>

View file

@ -1,14 +1,26 @@
{% for state in games %} {% for state in games %}
<div class="game_state"> <section class="accordion">
<div class="info"> <input type="checkbox" name="collapse" id="handle_{{loop.index}}">
<p>{{state.name}}</p> <h2 class="handle">
{% if state.finished %}<p>Finished</p> {% endif %} <label for="handle_{{loop.index}}">
{% if state.turns %}<p>Turns: {{ state.turns }} </p> {% endif %} <span>{{state.name}} ({{state.map}})</span>
</div> <span style="float: right">{% if state.type == "Finished" %}Done{% else %}{{ state.connected }}/{{state.total}}{% endif %}</span>
</label>
</h2>
<div class="content">
{% if state.type == "Playing" %}
<div class="players"> <div class="players">
{% for player in state.players %} {% for player in state.players %}
<p>{{ player }}</p> <p class="{% if player.waiting %}waiting {% endif %}{% if player.connected %}connected {% endif %}{% if player.reconnecting %}reconnecting {% endif %}">{{ player.value }}</p>
{% endfor %} {% endfor %}
</div> </div>
</div> {% else %}
<div class="players">
{% for player in state.players %}
<p class="{% if player[1] %}winner{% endif %}">{{ player[0] }}</p>
{% endfor %}
</div>
{% endif %}
</div>
</section>
{% endfor %} {% endfor %}

View file

@ -1,2 +1,2 @@
{"nop":2,"name":"Dick But","map":"maps/hex.json","max_turns":2000} {"nop":2,"name":"Bexit","map":"maps/hex.json","max_turns":200}

View file

@ -1,10 +1,16 @@
import requests, json, subprocess, os import requests, json, subprocess, os
host = os.getenv("HOST") or "localhost:8000" host = os.getenv("HOST") or "localhost:8000"
headers = {'content-type': 'application/json'} headers = {'content-type': 'application/json'}
try:
r = requests.post(f"https://{host}/lobby", data=open('game_start.json').read(), headers=headers)
data = r.json()
except Exception:
r = requests.post(f"http://{host}/lobby", data=open('game_start.json').read(), headers=headers) r = requests.post(f"http://{host}/lobby", data=open('game_start.json').read(), headers=headers)
data = r.json() data = r.json()
processes = [] processes = []
for player in data["players"]: for player in data["players"]:

View file

@ -15,10 +15,9 @@ def execute(cmd):
raise subprocess.CalledProcessError(return_code, cmd) raise subprocess.CalledProcessError(return_code, cmd)
def connect(host, port, id): def connect(host, port, id):
print(host, port)
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((host, port)) s.connect((host, port))
s.sendall(f"{id.strip()}\n".encode("utf8")) s.sendall(f"{json.dumps(id)}\n".encode("utf8"))
return s return s
def handle_input(it, socket): def handle_input(it, socket):
@ -33,11 +32,13 @@ def main():
help='What host to connect to') help='What host to connect to')
parser.add_argument('--port', '-p', default=6666, type=int, parser.add_argument('--port', '-p', default=6666, type=int,
help='What port to connect to') help='What port to connect to')
parser.add_argument('--name', '-n', default="Silvius",
help='Who are you?')
parser.add_argument('arguments', nargs=argparse.REMAINDER, parser.add_argument('arguments', nargs=argparse.REMAINDER,
help='How to run the bot') help='How to run the bot')
args = parser.parse_args() args = parser.parse_args()
sock = connect(args.host, args.port, args.id) sock = connect(args.host, args.port, {"id": int(args.id), "name": args.name})
f = sock.makefile("rw") f = sock.makefile("rw")
it = execute(args.arguments) it = execute(args.arguments)
@ -48,6 +49,7 @@ def main():
line = f.readline() line = f.readline()
content = "Nothing" content = "Nothing"
while line: while line:
print(line)
content = json.loads(line) content = json.loads(line)
if content["type"] == "game_state": if content["type"] == "game_state":
stdin.write(json.dumps(content["content"])+"\n") stdin.write(json.dumps(content["content"])+"\n")