Refactor before it is too late

This commit is contained in:
ajuvercr 2020-04-09 22:57:12 +02:00
parent 5404f4256a
commit 6b804724b4
13 changed files with 312 additions and 254 deletions

View file

@ -33,7 +33,6 @@ use futures::future::FutureExt;
use mozaic::graph; use mozaic::graph;
use mozaic::modules::*; use mozaic::modules::*;
mod info;
mod planetwars; mod planetwars;
mod routes; mod routes;
mod util; mod util;
@ -45,6 +44,8 @@ use rocket_contrib::templates::tera::{self, Value};
use std::collections::HashMap; use std::collections::HashMap;
/// Calculate viewbox from array of points (used in map preview), added to Tera engine.
/// So this function can be called in template.
fn calc_viewbox(value: Value, _: HashMap<String, Value>) -> tera::Result<Value> { fn calc_viewbox(value: Value, _: HashMap<String, Value>) -> tera::Result<Value> {
let mut min_x = std::f64::MAX; let mut min_x = std::f64::MAX;
let mut min_y = std::f64::MAX; let mut min_y = std::f64::MAX;
@ -62,10 +63,12 @@ fn calc_viewbox(value: Value, _: HashMap<String, Value>) -> tera::Result<Value>
return Ok(Value::String(format!("{} {} {} {}", min_x - 3., min_y - 3., (max_x - min_x) + 6., (max_y - min_y) + 6.))); return Ok(Value::String(format!("{} {} {} {}", min_x - 3., min_y - 3., (max_x - min_x) + 6., (max_y - min_y) + 6.)));
} }
/// Get's the right colour for planets
fn get_colour(value: Value, _: HashMap<String, Value>) -> tera::Result<Value> { fn get_colour(value: Value, _: HashMap<String, Value>) -> tera::Result<Value> {
return Ok(Value::String(COLOURS[value.as_u64().unwrap_or(0) as usize].to_string())); return Ok(Value::String(COLOURS[value.as_u64().unwrap_or(0) as usize].to_string()));
} }
/// Async main function, starting logger, graph and rocket
#[async_std::main] #[async_std::main]
async fn main() { async fn main() {
let fut = graph::set_default(); let fut = graph::set_default();
@ -81,7 +84,6 @@ async fn main() {
let mut routes = Vec::new(); let mut routes = Vec::new();
routes::fuel(&mut routes); routes::fuel(&mut routes);
info::fuel(&mut routes);
let tera = Template::custom(|engines: &mut Engines| { let tera = Template::custom(|engines: &mut Engines| {
engines.tera.register_filter("calc_viewbox", calc_viewbox); engines.tera.register_filter("calc_viewbox", calc_viewbox);
@ -98,6 +100,8 @@ async fn main() {
.unwrap(); .unwrap();
} }
/// Creates the actual game_manager
/// Opening tcp socket etc..
async 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());

View file

@ -7,22 +7,24 @@ use crate::util::*;
const MAX: usize = 6; const MAX: usize = 6;
/// Redirects to the first info page
#[get("/info")] #[get("/info")]
fn help_base() -> Redirect { fn info_base() -> Redirect {
Redirect::to("/info/1") Redirect::to("/info/1")
} }
/// Renders the <page> info page
#[get("/info/<page>")] #[get("/info/<page>")]
async fn help(page: usize) -> Template { async fn info(page: usize) -> Template {
let context = Context::new_with("info", json!({ let context = Context::new_with("info", json!({
"page": page, "page": page,
"next": if page + 1 <= MAX { Some(page + 1) } else { None }, "next": if page + 1 <= MAX { Some(page + 1) } else { None },
"prev": if page - 1 > 0 { Some(page - 1) } else { None } "prev": if page - 1 > 0 { Some(page - 1) } else { None }
})); }));
Template::render(format!("help/help_{}", page), &context) Template::render(format!("info/info_{}", page), &context)
} }
pub fn fuel(routes: &mut Vec<Route>) { pub fn fuel(routes: &mut Vec<Route>) {
routes.extend(routes![help_base, help]); routes.extend(routes![info_base, info]);
} }

View file

@ -1,102 +1,25 @@
use crate::planetwars;
use serde::{Deserialize};
use serde_json::Value;
use rocket::{Route, State};
use rocket::response::NamedFile;
use rocket_contrib::templates::Template;
use rocket_contrib::json::Json;
use async_std::prelude::*;
use async_std::fs;
use async_std::io::ReadExt;
use crate::util::*; use crate::util::*;
use std::path::Path; use rocket::{State, Route};
use rocket_contrib::json::Json;
use rocket_contrib::templates::Template;
#[get("/<file..>", rank = 6)] use mozaic::modules::types::*;
async fn files(file: PathBuf) -> Option<NamedFile> { use mozaic::modules::{game, StepLock};
NamedFile::open(Path::new("static/").join(file)).ok() use mozaic::util::request::Connect;
}
#[get("/")] use async_std::fs;
async fn index() -> Template { use async_std::prelude::StreamExt;
// let context = context();
let context = Context::new("Home");
// context.insert("name".to_string(), "Arthur".to_string());
Template::render("index", &context)
}
#[derive(Deserialize, Debug)] use futures::executor::ThreadPool;
struct MapReq {
pub name: String,
pub map: crate::planetwars::Map,
}
use std::path::PathBuf; use serde_json::Value;
#[post("/maps", data="<map_req>")]
async fn map_post(map_req: Json<MapReq>) -> Result<String, String> {
let MapReq { name, map } = map_req.into_inner();
let path: PathBuf = PathBuf::from(format!("maps/{}.json", name));
if path.exists() {
return Err("File already exists!".into());
}
let mut file = fs::File::create(path).await.map_err(|_| "IO error".to_string())?;
file.write_all(&serde_json::to_vec_pretty(&map).unwrap()).await.map_err(|_| "IO error".to_string())?;
Ok("ok".into())
}
#[get("/lobby")]
async fn lobby_get(gm: State<'_, game::Manager>, state: State<'_, Games>) -> Result<Template, String> {
let maps = get_maps().await?;
let games = get_states(&state.get_games(), &gm).await?;
let context = Context::new_with("Lobby", Lobby { games, maps });
Ok(Template::render("lobby", &context))
}
#[get("/mapbuilder")]
async fn builder_get() -> Result<Template, String> {
let context = Context::new("Map Builder");
Ok(Template::render("mapbuilder", &context))
}
#[get("/debug")]
async fn debug_get() -> Result<Template, String> {
let context = Context::new("Debug Station");
Ok(Template::render("debug", &context))
}
#[get("/visualizer")]
async fn visualizer_get() -> Template {
let game_options = get_games().await;
let context = Context::new_with("Visualizer", json!({"games": game_options, "colours": COLOURS}));
Template::render("visualizer", &context)
}
#[get("/maps/<file>")]
async fn map_get(file: String) -> Result<Template, String> {
let mut content = String::new();
let mut file = fs::File::open(Path::new("maps/").join(file)).await.map_err(|_| "IO ERROR".to_string())?;
file.read_to_string(&mut content).await.map_err(|_| "IO ERROR".to_string())?;
Ok(Template::render("map_partial", &serde_json::from_str::<serde_json::Value>(&content).unwrap()))
}
#[get("/partial/state")]
async fn state_get(gm: State<'_, game::Manager>, state: State<'_, Games>) -> Result<Template, String> {
let games = get_states(&state.get_games(), &gm).await?;
let context = Context::new_with("Lobby", Lobby { games, maps: Vec::new() });
Ok(Template::render("state_partial", &context))
}
use rand::prelude::*;
/// The type required to build a game.
/// (json in POST request).
#[derive(Deserialize, Debug)] #[derive(Deserialize, Debug)]
struct GameReq { struct GameReq {
nop: u64, nop: u64,
@ -105,15 +28,35 @@ struct GameReq {
name: String, name: String,
} }
/// Response when building a game.
#[derive(Serialize)] #[derive(Serialize)]
struct GameRes { struct GameRes {
players: Vec<u64>, players: Vec<u64>,
state: Value, state: Value,
} }
use mozaic::util::request::Connect; /// Standard get function for the lobby tab
#[get("/lobby")]
async fn get_lobby(gm: State<'_, game::Manager>, state: State<'_, Games>) -> Result<Template, String> {
let maps = get_maps().await?;
let games = get_states(&state.get_games(), &gm).await?;
let context = Context::new_with("Lobby", Lobby { games, maps });
Ok(Template::render("lobby", &context))
}
/// The lobby get's this automatically on load and on refresh.
#[get("/partial/state")]
async fn state_get(gm: State<'_, game::Manager>, state: State<'_, Games>) -> Result<Template, String> {
let games = get_states(&state.get_games(), &gm).await?;
let context = Context::new_with("Lobby", Lobby { games, maps: Vec::new() });
Ok(Template::render("state_partial", &context))
}
/// Post function to create a game.
/// Returns the keys of the players in json.
#[post("/lobby", data="<game_req>")] #[post("/lobby", data="<game_req>")]
async fn game_post(game_req: Json<GameReq>, tp: State<'_, ThreadPool>, gm: State<'_, game::Manager>, state: State<'_, Games>) -> Result<Json<GameRes>, String> { async fn post_game(game_req: Json<GameReq>, tp: State<'_, ThreadPool>, gm: State<'_, game::Manager>, state: State<'_, Games>) -> Result<Json<GameRes>, String> {
let game = build_builder(tp.clone(), game_req.nop, game_req.max_turns, &game_req.map, &game_req.name); let game = build_builder(tp.clone(), game_req.nop, game_req.max_turns, &game_req.map, &game_req.name);
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);
@ -136,17 +79,7 @@ async fn game_post(game_req: Json<GameReq>, tp: State<'_, ThreadPool>, gm: State
} }
} }
pub fn fuel(routes: &mut Vec<Route>) { /// Generate random ID for the game, used as filename
routes.extend(routes![files, index, map_post, map_get, lobby_get, builder_get, visualizer_get, game_post, state_get, debug_get]);
}
use crate::planetwars;
use mozaic::modules::types::*;
use mozaic::modules::{game, StepLock};
use futures::executor::ThreadPool;
use rand::prelude::*;
fn generate_string_id() -> String { fn generate_string_id() -> String {
rand::thread_rng() rand::thread_rng()
.sample_iter(&rand::distributions::Alphanumeric) .sample_iter(&rand::distributions::Alphanumeric)
@ -154,6 +87,8 @@ fn generate_string_id() -> String {
.collect::<String>() + ".json" .collect::<String>() + ".json"
} }
/// game::Manager spawns game::Builder to start games.
/// This returns such a Builder for a planetwars game.
fn build_builder( fn build_builder(
pool: ThreadPool, pool: ThreadPool,
number_of_clients: u64, number_of_clients: u64,
@ -176,3 +111,83 @@ fn build_builder(
.with_timeout(std::time::Duration::from_secs(1)), .with_timeout(std::time::Duration::from_secs(1)),
) )
} }
/// Fuels the lobby routes
pub fn fuel(routes: &mut Vec<Route>) {
routes.extend(routes![post_game, get_lobby, state_get]);
}
#[derive(Serialize)]
pub struct Lobby {
pub games: Vec<GameState>,
pub maps: Vec<Map>,
}
#[derive(Serialize)]
pub struct Map {
name: String,
url: String,
}
async fn get_maps() -> Result<Vec<Map>, String> {
let mut maps = Vec::new();
let mut entries = fs::read_dir("maps")
.await
.map_err(|_| "IO error".to_string())?;
while let Some(file) = entries.next().await {
let file = file.map_err(|_| "IO error".to_string())?.path();
if let Some(stem) = file.file_stem().and_then(|x| x.to_str()) {
maps.push(Map {
name: stem.to_string(),
url: file.to_str().unwrap().to_string(),
});
}
}
Ok(maps)
}
use crate::planetwars::FinishedState;
use futures::future::{join_all, FutureExt};
pub async fn get_states(
game_ids: &Vec<(String, u64)>,
manager: &game::Manager,
) -> Result<Vec<GameState>, String> {
let mut states = Vec::new();
let gss = join_all(
game_ids
.iter()
.cloned()
.map(|(name, id)| manager.get_state(id).map(move |f| (f, name))),
)
.await;
for (gs, name) in gss {
if let Some(state) = gs {
match state {
Ok((state, conns)) => {
let players: Vec<PlayerStatus> =
conns.iter().cloned().map(|x| x.into()).collect();
let connected = players.iter().filter(|x| x.connected).count();
states.push(GameState::Playing {
name: name,
total: players.len(),
players,
connected,
map: String::new(),
state,
});
}
Err(value) => {
let state: FinishedState = serde_json::from_value(value).expect("Shit failed");
states.push(state.into());
}
}
}
}
states.sort();
Ok(states)
}

View file

@ -0,0 +1,50 @@
use serde::{Deserialize};
use rocket::{Route};
use rocket_contrib::templates::Template;
use rocket_contrib::json::Json;
use async_std::prelude::*;
use async_std::fs;
use async_std::io::ReadExt;
use std::path::{Path, PathBuf};
/// The expected body to create a map.
#[derive(Deserialize, Debug)]
struct MapReq {
pub name: String,
pub map: crate::planetwars::Map,
}
/// Post route to create a map.
#[post("/maps", data="<map_req>")]
async fn map_post(map_req: Json<MapReq>) -> Result<String, String> {
let MapReq { name, map } = map_req.into_inner();
let path: PathBuf = PathBuf::from(format!("maps/{}.json", name));
if path.exists() {
return Err("File already exists!".into());
}
let mut file = fs::File::create(path).await.map_err(|_| "IO error".to_string())?;
file.write_all(&serde_json::to_vec_pretty(&map).unwrap()).await.map_err(|_| "IO error".to_string())?;
Ok("ok".into())
}
/// Map partial, rendering a map as svg and returning the svg element
/// Used in the lobby page for the map previewer
#[get("/maps/<file>")]
async fn map_get(file: String) -> Result<Template, String> {
let mut content = String::new();
let mut file = fs::File::open(Path::new("maps/").join(file)).await.map_err(|_| "IO ERROR".to_string())?;
file.read_to_string(&mut content).await.map_err(|_| "IO ERROR".to_string())?;
Ok(Template::render("map_partial", &serde_json::from_str::<serde_json::Value>(&content).unwrap()))
}
pub fn fuel(routes: &mut Vec<Route>) {
routes.extend(routes![map_post, map_get]);
}

82
backend/src/routes/mod.rs Normal file
View file

@ -0,0 +1,82 @@
use crate::util::*;
use crate::planetwars::FinishedState;
use rocket::{Route};
use rocket::response::NamedFile;
use rocket_contrib::templates::Template;
use async_std::prelude::*;
use async_std::io::BufReader;
use async_std::fs;
use futures::stream::StreamExt;
use std::path::{Path, PathBuf};
mod lobby;
mod maps;
mod info;
/// Handles all files located in the static folder
#[get("/<file..>", rank = 6)]
async fn files(file: PathBuf) -> Option<NamedFile> {
NamedFile::open(Path::new("static/").join(file)).ok()
}
/// Routes the index page, rendering the index Template.
#[get("/")]
async fn index() -> Template {
let context = Context::new("Home");
Template::render("index", &context)
}
/// Routes the mapbuilder page, rendering the mapbuilder Template.
#[get("/mapbuilder")]
async fn builder_get() -> Result<Template, String> {
let context = Context::new("Map Builder");
Ok(Template::render("mapbuilder", &context))
}
/// Routes the debug page, rendering the debug Template.
#[get("/debug")]
async fn debug_get() -> Result<Template, String> {
let context = Context::new("Debug Station");
Ok(Template::render("debug", &context))
}
/// Routes the visualizer page, rendering the visualizer Template.
#[get("/visualizer")]
async fn visualizer_get() -> Template {
let game_options = get_played_games().await;
let context = Context::new_with("Visualizer", json!({"games": game_options, "colours": COLOURS}));
Template::render("visualizer", &context)
}
/// Fuels all routes
pub fn fuel(routes: &mut Vec<Route>) {
routes.extend(routes![files, index, builder_get, visualizer_get, debug_get]);
lobby::fuel(routes);
maps::fuel(routes);
info::fuel(routes);
}
/// Reads games.ini
/// File that represents all played games
/// Ready to be visualized
async fn get_played_games() -> Vec<GameState> {
match fs::File::open("games.ini").await {
Ok(file) => {
let file = BufReader::new(file);
file.lines()
.filter_map(move |maybe| async {
maybe
.ok()
.and_then(|line| serde_json::from_str::<FinishedState>(&line).ok())
})
.map(|state| state.into())
.collect()
.await
}
Err(_) => Vec::new(),
}
}

View file

@ -1,5 +1,9 @@
use async_std::fs; use crate::planetwars::FinishedState;
use async_std::prelude::*; use mozaic::util::request::Connect;
use serde_json::Value;
use std::cmp::Ordering;
use std::sync::{Arc, Mutex};
static NAV: [(&'static str, &'static str); 6] = [ static NAV: [(&'static str, &'static str); 6] = [
("/", "Home"), ("/", "Home"),
@ -14,18 +18,14 @@ pub static COLOURS: [&'static str; 9] = [
"gray", "blue", "cyan", "green", "yellow", "orange", "red", "pink", "purple", "gray", "blue", "cyan", "green", "yellow", "orange", "red", "pink", "purple",
]; ];
#[derive(Serialize)] /// The state of a player, in a running game.
pub struct Map { /// This represents actual players or connection keys.
name: String,
url: String,
}
#[derive(Serialize, Eq, PartialEq)] #[derive(Serialize, Eq, PartialEq)]
pub struct PlayerStatus { pub struct PlayerStatus {
waiting: bool, pub waiting: bool,
connected: bool, pub connected: bool,
reconnecting: bool, pub reconnecting: bool,
value: String, pub value: String,
} }
impl From<Connect> for PlayerStatus { impl From<Connect> for PlayerStatus {
fn from(value: Connect) -> Self { fn from(value: Connect) -> Self {
@ -53,8 +53,9 @@ impl From<Connect> for PlayerStatus {
} }
} }
use serde_json::Value; /// The GameState is the state of a game.
/// Either Finished, so the game is done, not running, and there is a posible visualization.
/// Or Playing, the game is still being managed by the mozaic framework.
#[derive(Serialize, Eq, PartialEq)] #[derive(Serialize, Eq, PartialEq)]
#[serde(tag = "type")] #[serde(tag = "type")]
pub enum GameState { pub enum GameState {
@ -75,8 +76,6 @@ pub enum GameState {
}, },
} }
use std::cmp::Ordering;
impl PartialOrd for GameState { impl PartialOrd for GameState {
fn partial_cmp(&self, other: &GameState) -> Option<Ordering> { fn partial_cmp(&self, other: &GameState) -> Option<Ordering> {
Some(self.cmp(other)) Some(self.cmp(other))
@ -104,10 +103,10 @@ impl From<FinishedState> for GameState {
GameState::Finished { GameState::Finished {
players: state players: state
.players .players
.iter() .iter()
.map(|(id, name)| (name.clone(), state.winners.contains(&id))) .map(|(id, name)| (name.clone(), state.winners.contains(&id)))
.collect(), .collect(),
map: state.map, map: state.map,
name: state.name, name: state.name,
turns: state.turns, turns: state.turns,
@ -116,6 +115,7 @@ impl From<FinishedState> for GameState {
} }
} }
/// Link struct, holding all necessary information
#[derive(Serialize)] #[derive(Serialize)]
struct Link { struct Link {
name: String, name: String,
@ -123,18 +123,21 @@ struct Link {
active: bool, active: bool,
} }
#[derive(Serialize)] impl Link {
pub struct Lobby { fn build_nav(active: &str) -> Vec<Link> {
pub games: Vec<GameState>, NAV.iter()
pub maps: Vec<Map>, .map(|(href, name)| Link {
name: name.to_string(),
href: href.to_string(),
active: *name == active,
})
.collect()
}
} }
// #[derive(Serialize)] /// Context used as template context.
// #[serde(rename_all = "camelCase")] /// This way you know to add nav bar support etc.
// pub enum ContextT { /// This T can be anything that is serializable, like json!({}) macro.
// Games(Vec<GameState>),
// }
#[derive(Serialize)] #[derive(Serialize)]
pub struct Context<T> { pub struct Context<T> {
pub name: String, pub name: String,
@ -146,14 +149,7 @@ pub struct Context<T> {
impl<T> Context<T> { impl<T> Context<T> {
pub fn new_with(active: &str, t: T) -> Self { pub fn new_with(active: &str, t: T) -> Self {
let nav = NAV let nav = Link::build_nav(active);
.iter()
.map(|(href, name)| Link {
name: name.to_string(),
href: href.to_string(),
active: *name == active,
})
.collect();
Context { Context {
nav, nav,
@ -165,14 +161,7 @@ impl<T> Context<T> {
impl Context<()> { impl Context<()> {
pub fn new(active: &str) -> Self { pub fn new(active: &str) -> Self {
let nav = NAV let nav = Link::build_nav(active);
.iter()
.map(|(href, name)| Link {
name: name.to_string(),
href: href.to_string(),
active: *name == active,
})
.collect();
Context { Context {
nav, nav,
@ -182,92 +171,8 @@ impl Context<()> {
} }
} }
pub async fn get_maps() -> Result<Vec<Map>, String> {
let mut maps = Vec::new();
let mut entries = fs::read_dir("maps")
.await
.map_err(|_| "IO error".to_string())?;
while let Some(file) = entries.next().await {
let file = file.map_err(|_| "IO error".to_string())?.path();
if let Some(stem) = file.file_stem().and_then(|x| x.to_str()) {
maps.push(Map {
name: stem.to_string(),
url: file.to_str().unwrap().to_string(),
});
}
}
Ok(maps)
}
use async_std::io::BufReader;
use futures::stream::StreamExt;
pub async fn get_games() -> Vec<GameState> {
match fs::File::open("games.ini").await {
Ok(file) => {
let file = BufReader::new(file);
file.lines()
.filter_map(move |maybe| async {
maybe
.ok()
.and_then(|line| serde_json::from_str::<FinishedState>(&line).ok())
})
.map(|state| state.into())
.collect()
.await
}
Err(_) => Vec::new(),
}
}
use crate::planetwars::FinishedState;
use futures::future::{join_all, FutureExt};
use mozaic::modules::game;
use mozaic::util::request::Connect;
pub async fn get_states(
game_ids: &Vec<(String, u64)>,
manager: &game::Manager,
) -> Result<Vec<GameState>, String> {
let mut states = Vec::new();
let gss = join_all(
game_ids
.iter()
.cloned()
.map(|(name, id)| manager.get_state(id).map(move |f| (f, name))),
)
.await;
for (gs, name) in gss {
if let Some(state) = gs {
match state {
Ok((state, conns)) => {
let players: Vec<PlayerStatus> =
conns.iter().cloned().map(|x| x.into()).collect();
let connected = players.iter().filter(|x| x.connected).count();
states.push(GameState::Playing {
name: name,
total: players.len(),
players,
connected,
map: String::new(),
state,
});
}
Err(value) => {
let state: FinishedState = serde_json::from_value(value).expect("Shit failed");
states.push(state.into());
}
}
}
}
states.sort();
Ok(states)
}
use std::sync::{Arc, Mutex};
/// Games is the game manager wrapper so Rocket can manage it
pub struct Games { pub struct Games {
inner: Arc<Mutex<Vec<(String, u64)>>>, inner: Arc<Mutex<Vec<(String, u64)>>>,
} }

View file

@ -20,7 +20,7 @@
</div> </div>
<hr style="width:100%"> <hr style="width:100%">
<div class="help_content"> <div class="help_content">
{% block help %}{% endblock help %} {% block info %}{% endblock info %}
</div> </div>
</div> </div>

View file

@ -1,4 +1,4 @@
{% extends "help/base" %} {% extends "info/base" %}
{% block header %} {% block header %}
@ -7,7 +7,7 @@ Information
{% endblock %} {% endblock %}
{% block help %} {% block info %}
<div class="help_content_2"> <div class="help_content_2">
<h2>Rules</h2> <h2>Rules</h2>

View file

@ -1,4 +1,4 @@
{% extends "help/base" %} {% extends "info/base" %}
{% block header %} {% block header %}
@ -7,7 +7,7 @@ Information
{% endblock %} {% endblock %}
{% block help %} {% block info %}
<div class="help_content_2"> <div class="help_content_2">
<h2>Combat resolution</h2> <h2>Combat resolution</h2>

View file

@ -1,4 +1,4 @@
{% extends "help/base" %} {% extends "info/base" %}
{% block header %} {% block header %}
@ -7,7 +7,7 @@ Interaction with the game
{% endblock %} {% endblock %}
{% block help %} {% block info %}
<div class="help_content_1"> <div class="help_content_1">
<div class="boxed centering"> <div class="boxed centering">

View file

@ -1,4 +1,4 @@
{% extends "help/base" %} {% extends "info/base" %}
{% block header %} {% block header %}
@ -7,7 +7,7 @@ Format: Game state
{% endblock %} {% endblock %}
{% block help %} {% block info %}
<div class="help_content_2"> <div class="help_content_2">
<pre> <pre>

View file

@ -1,4 +1,4 @@
{% extends "help/base" %} {% extends "info/base" %}
{% block header %} {% block header %}
@ -7,7 +7,7 @@ Format: Player turn (actions)
{% endblock %} {% endblock %}
{% block help %} {% block info %}
<div class="help_content_2"> <div class="help_content_2">
<pre> <pre>

View file

@ -1,4 +1,4 @@
{% extends "help/base" %} {% extends "info/base" %}
{% block header %} {% block header %}
@ -7,7 +7,7 @@ How to connect
{% endblock %} {% endblock %}
{% block help %} {% block info %}
<div class="help_content_2"> <div class="help_content_2">