Skip to content

Commit

Permalink
Merge pull request #68 from bcollazo/bryan/dev
Browse files Browse the repository at this point in the history
Simple Tick Playing and Show Cards
  • Loading branch information
bcollazo committed Apr 23, 2021
2 parents 20b11a1 + 1d52b7a commit 514d240
Show file tree
Hide file tree
Showing 7 changed files with 224 additions and 43 deletions.
14 changes: 14 additions & 0 deletions catanatron/json.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,18 @@
from catanatron.models.decks import Deck


def longest_roads_by_player(state):
roads = {
player.color.value: state.board.continuous_roads_by_player(player.color)
for player in state.players
}

return {
key: 0 if len(value) == 0 else max(map(len, value))
for key, value in roads.items()
}


class GameEncoder(json.JSONEncoder):
def default(self, obj):
if obj is None:
Expand Down Expand Up @@ -51,6 +63,8 @@ def default(self, obj):
"current_color": obj.state.current_player().color,
"current_prompt": obj.state.current_prompt,
"current_playable_actions": obj.state.playable_actions,
# TODO: Use cached value when we cache it.
"longest_roads_by_player": longest_roads_by_player(obj.state),
}
if isinstance(obj, Deck):
return {resource.value: count for resource, count in obj.cards.items()}
Expand Down
9 changes: 9 additions & 0 deletions catanatron_server/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,14 @@
CORS(app)


def advance_until_color(game, color, callbacks):
while (
game.winning_player() is not None and game.state.current_player().color != color
):
game.play_tick(callbacks)
print(game.winning_player(), game.state.current_player().color, color)


@app.route("/games/<string:game_id>/actions", methods=["POST"])
def tick_game(game_id):
game = get_last_game_state(game_id)
Expand Down Expand Up @@ -44,6 +52,7 @@ def create_game():
]
)
save_game_state(game)
advance_until_color(game, Color.BLUE, [lambda g: save_game_state(g)])
return jsonify({"game_id": game.id})


Expand Down
5 changes: 3 additions & 2 deletions ui/src/pages/ActionsToolbar.scss
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
@import "../variables.scss";

.actions-toolbar {
width: 100%;
max-width: 375px;
max-width: $sm-breakpoint;
margin-left: auto;
margin-right: auto;
padding: 10px;
Expand All @@ -11,7 +13,6 @@
button {
width: 60px;
height: 60px;
margin: 4px;
}

.confirm-btn {
Expand Down
149 changes: 122 additions & 27 deletions ui/src/pages/GameScreen.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { useParams } from "react-router-dom";
import axios from "axios";
import PropTypes from "prop-types";
import Loader from "react-loader-spinner";
import cn from "classnames";

import { API_URL } from "../configuration";
import ZoomableBoard from "./ZoomableBoard";
Expand All @@ -11,48 +12,136 @@ import ActionsToolbar from "./ActionsToolbar";
import "react-loader-spinner/dist/loader/css/react-spinner-loader.css";
import "./GameScreen.scss";

function GameScreen(props) {
const HUMAN_COLOR = "BLUE";

function Prompt({ actionQueue, state }) {
let prompt = "";
if (actionQueue.length === 0) {
prompt = `${state.current_color}: ${state.current_prompt}`;
} else {
prompt = `${actionQueue[0][0]}: ${actionQueue[0].slice(1)}`;
}
return <div className="prompt">{state && prompt}</div>;
}

function PlayerStateBox({ playerState, longestRoad }) {
const numDevCards = Object.values(playerState.development_deck).reduce(
(a, b) => a + b,
0
);
const resdeck = playerState.resource_deck;
return (
<div className="player-state-box">
<div className="resource-cards" title="Resource Cards">
{resdeck.WOOD !== 0 && (
<div className="wood-cards center-text">{resdeck.WOOD}</div>
)}
{resdeck.BRICK !== 0 && (
<div className="brick-cards center-text">{resdeck.BRICK}</div>
)}
{resdeck.SHEEP !== 0 && (
<div className="sheep-cards center-text">{resdeck.SHEEP}</div>
)}
{resdeck.WHEAT !== 0 && (
<div className="wheat-cards center-text">{resdeck.WHEAT}</div>
)}
{resdeck.ORE !== 0 && (
<div className="ore-cards center-text">{resdeck.ORE}</div>
)}
{numDevCards !== 0 && (
<div className="dev-cards center-text" title="Development Cards">
{numDevCards}
</div>
)}
</div>
<div
className={cn("num-knights center-text", {
has_army: playerState.has_army,
})}
title="Knights Played"
>
<span>{playerState.played_development_cards.KNIGHT}</span>
<small>knights</small>
</div>
<div
className={cn("num-roads center-text", {
has_road: playerState.has_road,
})}
title="Longest Road"
>
{longestRoad}
<small>roads</small>
</div>
<div
className={cn("victory-points center-text", {
has_road: playerState.actual_victory_points >= 10,
})}
title="Victory Points"
>
{playerState.actual_victory_points}
<small>VPs</small>
</div>
</div>
);
}

function getQueue(actions) {
const numActions = actions.length;
let i;
for (i = numActions - 1; i >= 0; i--) {
if (actions[i][0] === HUMAN_COLOR) {
break;
}
}
i++;
console.log(i, actions.slice(i, numActions));
return actions.slice(i, numActions);
}

function GameScreen() {
const { gameId } = useParams();
const [actionQueue, setActionQueue] = useState([]);
const [state, setState] = useState(null);
const automation = false;
// const [automation, setAutomation] = useState(false);
const [inFlightRequest, setInFlightRequest] = useState(false);

useEffect(() => {
(async () => {
const response = await fetch(API_URL + "/games/" + gameId);
const data = await response.json();
setState(data);
const response = await axios.get(API_URL + "/games/" + gameId);

const queue = getQueue(response.data.actions);
setActionQueue(queue);
setState(response.data);
})();
}, [gameId]);

const onClickNext = useCallback(async () => {
setInFlightRequest(true);
const response = await axios.post(`${API_URL}/games/${gameId}/actions`);
setInFlightRequest(false);
setState(response.data);
}, [gameId]);

// const onClickAutomation = () => {
// setAutomation(!automation);
// };
// If you queue, consume from queue, else populate
if (actionQueue.length > 0) {
setActionQueue(actionQueue.slice(1));
} else {
if (inFlightRequest) return; // this makes it idempotent
setInFlightRequest(true);
const response = await axios.post(`${API_URL}/games/${gameId}/actions`);
setInFlightRequest(false);

useEffect(() => {
const interval = setInterval(async () => {
if (automation && !inFlightRequest) {
await onClickNext();
}
}, 50);
return () => clearInterval(interval);
}, [automation, inFlightRequest, onClickNext]);
const queue = getQueue(response.data.actions);
setActionQueue(queue);
setState(response.data);
}
}, [gameId, inFlightRequest, setInFlightRequest, actionQueue]);

console.log(state);

const bot = state && state.players.find((x) => x.color !== HUMAN_COLOR);
const human = state && state.players.find((x) => x.color === HUMAN_COLOR);
return (
<main>
<div className="prompt">
{state && `${state.current_color}: ${state.current_prompt}`}
</div>
{state && <Prompt actionQueue={actionQueue} state={state} />}
{state && (
<PlayerStateBox
playerState={bot}
longestRoad={state.longest_roads_by_player[bot.color]}
/>
)}
{!state && (
<Loader
className="loader"
Expand All @@ -63,6 +152,12 @@ function GameScreen(props) {
/>
)}
{state && <ZoomableBoard state={state} />}
{state && (
<PlayerStateBox
playerState={human}
longestRoad={state.longest_roads_by_player[HUMAN_COLOR]}
/>
)}
<ActionsToolbar onTick={onClickNext} />
</main>
);
Expand Down
85 changes: 72 additions & 13 deletions ui/src/pages/GameScreen.scss
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
@import "../variables.scss";

.MuiToolbar-root {
background: #030000;
}
Expand All @@ -7,7 +9,7 @@ h6.MuiTypography-h6 {
}

main {
background: #030000;
background: #ffffff;

position: fixed;
left: 0;
Expand All @@ -28,17 +30,83 @@ main {
background: #161313;
color: #66bf4c;
font-family: monospace;
padding: $sm-gutter;

height: 60px;
text-align: center;
font-size: 1.5em;
font-size: 1.2rem;

text-align: left;
display: flex;
justify-content: center;
align-items: center;

overflow-x: auto;
white-space: nowrap;
}

.player-state-box {
height: 60px;
padding: $sm-gutter;

display: flex;
justify-content: space-between;
align-items: center;

max-width: $sm-breakpoint;
margin: 0 auto;
width: 100%;

.resource-cards {
flex-grow: 1;
display: flex;
gap: 6px;
}

.wood-cards {
background: greenyellow;
}
.brick-cards {
background: lightcoral;
}
.wheat-cards {
background: yellow;
}
.sheep-cards {
background: lightgreen;
}
.ore-cards {
background: lightsteelblue;
}
.dev-cards {
background: #aeaeae;
}

.center-text {
display: flex;
justify-content: center;
align-items: center;

width: 25px;
height: 40px;
}

.num-knights,
.num-roads,
.victory-points {
flex-direction: column;
width: 60px;
height: 40px;
}

.has_army,
.has_road {
font-weight: bold;
}
}

.board-container,
.loader {
height: calc(100% - 144px); // sync with ZoomableBoard
height: calc(100% - 144px - 120px); // sync with ZoomableBoard
width: 100%;
}

Expand All @@ -63,15 +131,6 @@ main {
.board.show {
opacity: 1;
}

.board .block {
position: absolute;
background-color: red;

// left: 50px;
// height: 50px;
// width: 50px;
}
}

.node {
Expand Down
3 changes: 2 additions & 1 deletion ui/src/pages/ZoomableBoard.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,8 @@ export default function ZoomableBoard({ state }) {
const { width, height } = useWindowSize();
const [show, setShow] = useState(false);

const containerHeight = height - 144;
// TODO: Keep in sync with CSS
const containerHeight = height - 144 - 120;

const center = [width / 2, containerHeight / 2];
const size = computeDefaultSize(width, containerHeight);
Expand Down
2 changes: 2 additions & 0 deletions ui/src/variables.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
$sm-breakpoint: 576px;
$sm-gutter: 10px;

0 comments on commit 514d240

Please sign in to comment.