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 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
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
35 changes: 35 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 All @@ -28,6 +29,12 @@ impl InMemoryStats {
}
}

#[derive(Serialize, Deserialize)]
pub struct PublicGameInfo {
name: String,
num_players: usize,
jimmyfang94 marked this conversation as resolved.
Show resolved Hide resolved
}

pub async fn load_dump_file<S: Storage<VersionedGame, E>, E: Send + std::fmt::Debug>(
logger: Logger,
backend_storage: S,
Expand Down Expand Up @@ -176,3 +183,31 @@ pub async fn dump_state(

Ok(Json(state_dump))
}

pub async fn public_games(
Extension(backend_storage): Extension<HashMapStorage<VersionedGame>>,
) -> Result<Json<Vec<PublicGameInfo>>, &'static str> {
let mut public_games: Vec<PublicGameInfo> = Vec::new();

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 let GameVisibility::Public = versioned_game.game.game_visibility() {
if let Ok(name) = String::from_utf8(room_name.clone()) {
public_games.push(PublicGameInfo {
name,
num_players: versioned_game.game.players().len(),
});
}
}
}
}

public_games.sort_by_key(|p| (-(p.num_players as isize), p.name.clone()));
Ok(Json(public_games))
}
11 changes: 8 additions & 3 deletions core/src/interactive.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,10 @@ 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,
GameModeSettings, GameShadowingPolicy, GameStartPolicy, GameVisibility, KittyBidPolicy,
KittyPenalty, KittyTheftPolicy, MultipleJoinPolicy, PlayTakebackPolicy, PropagatedState,
ThrowPenalty,
};

pub struct InteractiveGame {
state: GameState,
}
Expand Down Expand Up @@ -220,6 +220,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 +450,7 @@ pub enum Action {
SetShouldRevealKittyAtEndOfGame(bool),
SetHideThrowHaltingPlayer(bool),
SetTractorRequirements(TractorRequirements),
SetGameVisibility(GameVisibility),
StartGame,
DrawCard,
RevealCard,
Expand Down
10 changes: 7 additions & 3 deletions core/src/message.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,9 @@ 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,
GameShadowingPolicy, GameStartPolicy, GameVisibility, KittyBidPolicy, KittyPenalty,
KittyTheftPolicy, MultipleJoinPolicy, PlayTakebackPolicy, ThrowPenalty,
};

#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
#[serde(tag = "type")]
pub enum MessageVariant {
Expand Down Expand Up @@ -102,6 +101,9 @@ pub enum MessageVariant {
KittyTheftPolicySet {
policy: KittyTheftPolicy,
},
GameVisibilitySet {
visibility: GameVisibility,
},
TookBackPlay,
TookBackBid,
PlayedCards {
Expand Down Expand Up @@ -370,6 +372,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?),
})
}
}
34 changes: 34 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,20 @@ 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
9 changes: 8 additions & 1 deletion frontend/src/Credits.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ const contentStyle: React.CSSProperties = {
transform: "translate(-50%, -50%)",
};

const changeLogVersion: number = 21;
const changeLogVersion: number = 22;

const ChangeLog = (): JSX.Element => {
const [modalOpen, setModalOpen] = React.useState<boolean>(false);
Expand Down Expand Up @@ -59,6 +59,13 @@ const ChangeLog = (): JSX.Element => {
you&apos;re in the game.
</p>
<h2>Change Log</h2>
<p>2/24/2023:</p>
<ul>
<li>
Added the ability to list a room publicly for others to join (thanks
jimmyfang94!)
</li>
</ul>
<p>1/18/2023:</p>
<ul>
<li>Fixed performance issue when playing tricks with many cards</li>
Expand Down
20 changes: 20 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 @@ -1056,6 +1057,13 @@ const Initialize = (props: IProps): JSX.Element => {
},
});
break;
case "game_visibility":
send({
Action: {
SetGameVisibility: value,
},
});
break;
}
}
}
Expand Down Expand Up @@ -1291,6 +1299,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 setRoomName={props.setRoomName} />
</div>
);
};
Expand Down
103 changes: 103 additions & 0 deletions frontend/src/PublicRoomsPane.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
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: 200px;
`;
const Cell = styled.div`
display: table-cell;
`;

interface RowIProps {
roomName: string;
numPlayers: number;
setRoomName: (name: string, e: React.MouseEvent) => void;
}

const PublicRoomRow = ({
roomName,
numPlayers,
setRoomName,
}: RowIProps): JSX.Element => {
return (
<Row>
<Cell>
<button onClick={(e) => setRoomName(roomName, e)} className="normal">
{roomName}
</button>
</Cell>
<Cell>{numPlayers}</Cell>
</Row>
);
};

interface IProps {
setRoomName: (name: string) => void;
}

const PublicRoomsPane = (props: IProps): 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();
setPublicRooms(resultJSON);
};

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>
</div>
<div style={{ display: "table", borderSpacing: 10 }}>
<Row>
<LabelCell>Room Name</LabelCell>
<LabelCell>Players</LabelCell>
<LabelCell>
<button onClick={loadPublicRooms} className="normal">
Refresh
</button>
</LabelCell>
</Row>
{publicRooms.length === 0 && <Cell>No public rooms available</Cell>}
{publicRooms.map((roomInfo) => {
return (
<PublicRoomRow
key={roomInfo.name}
roomName={roomInfo.name}
numPlayers={roomInfo.num_players}
setRoomName={props.setRoomName}
/>
);
})}
</div>
</div>
);
};

export default PublicRoomsPane;
Loading