Skip to content
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

Implement Game Visibility And Add Public Rooms Panel on Front Page #423

Merged
merged 6 commits into from
Mar 1, 2023
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
3 changes: 2 additions & 1 deletion backend/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,8 @@ async fn main() -> Result<(), anyhow::Error> {
.route(
"/rules",
get(|| async { Redirect::permanent("/rules.html") }),
);
)
.route("/public_games.json", get(state_dump::public_games));

#[cfg(feature = "dynamic")]
let app = app.fallback_service(
Expand Down
25 changes: 25 additions & 0 deletions backend/src/state_dump.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ use slog::{error, info, o, Logger};
use tokio::sync::Mutex;

use shengji_core::game_state::GameState;
use shengji_core::settings::GameVisibility;
use shengji_types::GameMessage;
use storage::{HashMapStorage, Storage};

Expand Down Expand Up @@ -176,3 +177,27 @@ pub async fn dump_state(

Ok(Json(state_dump))
}

pub async fn public_games(
Extension(backend_storage): Extension<HashMapStorage<VersionedGame>>,
) -> Result<Json<HashMap<String, GameState>>, &'static str> {
let mut public_games: HashMap<String, GameState> = HashMap::new();
jimmyfang94 marked this conversation as resolved.
Show resolved Hide resolved

backend_storage.clone().prune().await;
let keys = backend_storage
.clone()
.get_all_keys()
.await
.map_err(|_| "failed to get ongoing games")?;
for room_name in keys {
if let Ok(versioned_game) = backend_storage.clone().get(room_name.clone()).await {
if versioned_game.game.game_visibility() == GameVisibility::Public {
jimmyfang94 marked this conversation as resolved.
Show resolved Hide resolved
if let Ok(name) = String::from_utf8(room_name.clone()) {
public_games.insert(name, versioned_game.game);
}
}
}
}

Ok(Json(public_games))
}
11 changes: 6 additions & 5 deletions core/src/interactive.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,7 @@ use shengji_mechanics::types::{Card, PlayerID, Rank};

use crate::game_state::{initialize_phase::InitializePhase, GameState};
use crate::message::MessageVariant;
use crate::settings::{
AdvancementPolicy, FirstLandlordSelectionPolicy, FriendSelection, FriendSelectionPolicy,
GameModeSettings, GameShadowingPolicy, GameStartPolicy, KittyBidPolicy, KittyPenalty,
KittyTheftPolicy, MultipleJoinPolicy, PlayTakebackPolicy, PropagatedState, ThrowPenalty,
};
use crate::settings::{AdvancementPolicy, FirstLandlordSelectionPolicy, FriendSelection, FriendSelectionPolicy, GameModeSettings, GameShadowingPolicy, GameStartPolicy, GameVisibility, KittyBidPolicy, KittyPenalty, KittyTheftPolicy, MultipleJoinPolicy, PlayTakebackPolicy, PropagatedState, ThrowPenalty};

pub struct InteractiveGame {
state: GameState,
Expand Down Expand Up @@ -220,6 +216,10 @@ impl InteractiveGame {
info!(logger, "Setting game mode"; "game_mode" => game_mode.variant());
state.set_game_mode(game_mode)?
}
(Action::SetGameVisibility(visibility), GameState::Initialize(ref mut state)) => {
info!(logger, "Setting game visibility"; "visibility" => visibility);
state.set_game_visibility(visibility)?
}
(Action::SetKittyPenalty(kitty_penalty), GameState::Initialize(ref mut state)) => {
info!(logger, "Setting kitty penalty"; "penalty" => kitty_penalty);
state.set_kitty_penalty(kitty_penalty)?
Expand Down Expand Up @@ -446,6 +446,7 @@ pub enum Action {
SetShouldRevealKittyAtEndOfGame(bool),
SetHideThrowHaltingPlayer(bool),
SetTractorRequirements(TractorRequirements),
SetGameVisibility(GameVisibility),
StartGame,
DrawCard,
RevealCard,
Expand Down
11 changes: 6 additions & 5 deletions core/src/message.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,7 @@ use shengji_mechanics::trick::{ThrowEvaluationPolicy, TractorRequirements, Trick
use shengji_mechanics::types::{Card, PlayerID, Rank};

use crate::game_state::play_phase::PlayerGameFinishedResult;
use crate::settings::{
AdvancementPolicy, FirstLandlordSelectionPolicy, FriendSelectionPolicy, GameModeSettings,
GameShadowingPolicy, GameStartPolicy, KittyBidPolicy, KittyPenalty, KittyTheftPolicy,
MultipleJoinPolicy, PlayTakebackPolicy, ThrowPenalty,
};
use crate::settings::{AdvancementPolicy, FirstLandlordSelectionPolicy, FriendSelectionPolicy, GameModeSettings, GameShadowingPolicy, GameStartPolicy, GameVisibility, KittyBidPolicy, KittyPenalty, KittyTheftPolicy, MultipleJoinPolicy, PlayTakebackPolicy, ThrowPenalty};

#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
#[serde(tag = "type")]
Expand Down Expand Up @@ -102,6 +98,9 @@ pub enum MessageVariant {
KittyTheftPolicySet {
policy: KittyTheftPolicy,
},
GameVisibilitySet {
visibility: GameVisibility,
},
TookBackPlay,
TookBackBid,
PlayedCards {
Expand Down Expand Up @@ -370,6 +369,8 @@ impl MessageVariant {
HideThrowHaltingPlayer { set: false } => format!("{} un-hid the player who prevents throws", n?),
TractorRequirementsChanged { tractor_requirements } =>
format!("{} required tractors to be at least {} cards wide by {} tuples long", n?, tractor_requirements.min_count, tractor_requirements.min_length),
GameVisibilitySet { visibility: GameVisibility::Public} => format!("{} listed the game publicly", n?),
GameVisibilitySet { visibility: GameVisibility::Unlisted} => format!("{} unlisted the game", n?),
})
}
}
32 changes: 32 additions & 0 deletions core/src/settings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -246,6 +246,20 @@ impl Default for GameStartPolicy {

shengji_mechanics::impl_slog_value!(GameStartPolicy);

#[derive(Debug, Copy, Clone, Eq, PartialEq, Serialize, Deserialize, JsonSchema)]
pub enum GameVisibility {
Public,
Unlisted,
}

impl Default for GameVisibility {
fn default() -> Self {
GameVisibility::Unlisted
}
}

shengji_mechanics::impl_slog_value!(GameVisibility);

#[derive(Debug, Copy, Clone, Eq, PartialEq, Serialize, Deserialize, JsonSchema)]
pub struct MaxRank(Rank);
shengji_mechanics::impl_slog_value!(MaxRank);
Expand Down Expand Up @@ -334,6 +348,8 @@ pub struct PropagatedState {
pub(crate) tractor_requirements: TractorRequirements,
#[serde(default)]
pub(crate) max_rank: MaxRank,
#[serde(default)]
pub(crate) game_visibility: GameVisibility,
}

impl PropagatedState {
Expand All @@ -357,6 +373,10 @@ impl PropagatedState {
self.num_decks.unwrap_or(self.players.len() / 2)
}

pub fn game_visibility(&self) -> GameVisibility {
self.game_visibility
}

pub fn decks(&self) -> Result<Vec<Deck>, Error> {
let mut decks = self.special_decks.clone();
let num_decks = self.num_decks();
Expand Down Expand Up @@ -790,6 +810,18 @@ impl PropagatedState {
}
}

pub fn set_game_visibility(
&mut self,
game_visibility: GameVisibility,
) -> Result<Vec<MessageVariant>, Error> {
if game_visibility != self.game_visibility {
self.game_visibility = game_visibility;
Ok(vec![MessageVariant::GameVisibilitySet { visibility: game_visibility }])
} else {
Ok(vec![])
}
}

pub fn set_user_multiple_game_session_policy(
&mut self,
policy: GameShadowingPolicy,
Expand Down
13 changes: 13 additions & 0 deletions frontend/src/Initialize.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -731,6 +731,7 @@ const Initialize = (props: IProps): JSX.Element => {
const setGameShadowingPolicy = onSelectString("SetGameShadowingPolicy");
const setGameStartPolicy = onSelectString("SetGameStartPolicy");
const setBidTakebackPolicy = onSelectString("SetBidTakebackPolicy");
const setGameVisibility = onSelectString("SetGameVisibility");

const setShouldRevealKittyAtEndOfGame = (
evt: React.ChangeEvent<HTMLSelectElement>
Expand Down Expand Up @@ -1291,6 +1292,18 @@ const Initialize = (props: IProps): JSX.Element => {
setPlayTakebackPolicy={setPlayTakebackPolicy}
setBidTakebackPolicy={setBidTakebackPolicy}
/>
<div>
<label>
Game Visibility{" "}
<select
value={props.state.propagated.game_visibility}
onChange={setGameVisibility}
>
<option value={"Unlisted"}>Unlisted</option>
<option value={"Public"}>Public</option>
</select>
</label>
</div>
jimmyfang94 marked this conversation as resolved.
Show resolved Hide resolved
<h3>Continuation settings</h3>
<LandlordSelector
players={props.state.propagated.players}
Expand Down
2 changes: 2 additions & 0 deletions frontend/src/JoinRoom.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import * as React from "react";
import { WebsocketContext } from "./WebsocketProvider";
import { TimerContext } from "./TimerProvider";
import LabeledPlay from "./LabeledPlay";
import PublicRoomsPane from "./PublicRoomsPane";

interface IProps {
name: string;
Expand Down Expand Up @@ -136,6 +137,7 @@ const JoinRoom = (props: IProps): JSX.Element => {
to you.
</p>
</div>
<PublicRoomsPane />
</div>
);
};
Expand Down
123 changes: 123 additions & 0 deletions frontend/src/PublicRoomsPane.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
import * as React from "react";
import { useEffect, useState } from "react";
import styled from "styled-components";

const Row = styled.div`
display: table-row;
line-height: 23px;
`;
const LabelCell = styled.div`
display: table-cell;
padding-right: 2em;
font-weight: bold;
width: 125px;
`;
const Cell = styled.div`
display: table-cell;
`;

interface RowIProps {
roomName: string;
numPlayers: number;
}

const PublicRoomRow = ({ roomName, numPlayers }: RowIProps): JSX.Element => {
return (
<Row>
<Cell>{roomName}</Cell>
<Cell>{numPlayers}</Cell>
</Row>
);
};

const PublicRoomsPane = (): JSX.Element => {
const [publicRooms, setPublicRooms] = useState([]);

useEffect(() => {
loadPublicRooms();
}, []);
const loadPublicRooms = (): void => {
try {
const fetchAsync = async (): Promise<void> => {
const fetchResult = await fetch("public_games.json");
const resultJSON = await fetchResult.json();
const resultArray = Object.entries(resultJSON);

// sort by number of players first, then name second
resultArray.sort((a: [string, any], b: [string, any]) => {
const aKey = a[0];
const aValue = a[1];
const bKey = b[0];
const bValue = b[1];

if (
aValue.Initialize === undefined ||
aValue.Initialize.propagated === undefined ||
aValue.Initialize.propagated.players === undefined ||
bValue.Initialize === undefined ||
bValue.Initialize.propagated === undefined ||
bValue.Initialize.propagated.players === undefined
) {
throw new Error(
`failed validation while sorting public rooms between ${aKey} ${bKey}`
);
}

const playerDiff =
bValue.Initialize.propagated.players.length -
aValue.Initialize.propagated.players.length;

if (playerDiff !== 0) {
return playerDiff;
}

return aKey.localeCompare(bKey);
});
setPublicRooms(resultArray);
};

fetchAsync().catch((e) => {
console.error(e);
});
} catch (err) {
console.log(err);
}
};

return (
<div className="">
<h3>Public Rooms</h3>
<div>
<p>
The games listed below are open to the public. Join them to find new
friends to play with!
</p>
<p>
Copy the room name into the input above, fill out your player name,
jimmyfang94 marked this conversation as resolved.
Show resolved Hide resolved
and click join to enter the room.
</p>
</div>
<div style={{ display: "table", borderSpacing: 10 }}>
<Row>
<LabelCell>Room Name</LabelCell>
<LabelCell>Players</LabelCell>
<LabelCell>
<button onClick={loadPublicRooms}> Refresh </button>
</LabelCell>
</Row>
{publicRooms.length === 0 && <div>No rooms available</div>}
{publicRooms.map(([key, value]) => {
return (
<PublicRoomRow
key={key}
roomName={key}
numPlayers={value.Initialize.propagated.players.length}
/>
);
})}
</div>
</div>
);
};

export default PublicRoomsPane;
10 changes: 10 additions & 0 deletions frontend/src/gen-types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,9 @@ export type Action =
| {
SetTractorRequirements: TractorRequirements;
}
| {
SetGameVisibility: GameVisibility;
}
| {
/**
* @minItems 2
Expand Down Expand Up @@ -206,6 +209,7 @@ export type BidTakebackPolicy = "AllowBidTakeback" | "NoBidTakeback";
export type KittyTheftPolicy = "AllowKittyTheft" | "NoKittyTheft";
export type GameShadowingPolicy = "AllowMultipleSessions" | "SingleSessionOnly";
export type GameStartPolicy = "AllowAnyPlayer" | "AllowLandlordOnly";
export type GameVisibility = "Public" | "Unlisted";
export type Card = string;
export type TrickUnit =
| {
Expand Down Expand Up @@ -449,6 +453,11 @@ export type MessageVariant =
type: "KittyTheftPolicySet";
[k: string]: unknown;
}
| {
type: "GameVisibilitySet";
visibility: GameVisibility;
[k: string]: unknown;
}
| {
type: "TookBackPlay";
[k: string]: unknown;
Expand Down Expand Up @@ -866,6 +875,7 @@ export interface PropagatedState {
game_scoring_parameters?: GameScoringParameters;
game_shadowing_policy?: GameShadowingPolicy & string;
game_start_policy?: GameStartPolicy & string;
game_visibility?: GameVisibility & string;
hide_landlord_points?: boolean;
hide_played_cards?: boolean;
hide_throw_halting_player?: boolean;
Expand Down
Loading