diff --git a/backend/rorapp/functions/__init__.py b/backend/rorapp/functions/__init__.py
index 7e211078..209a3e32 100644
--- a/backend/rorapp/functions/__init__.py
+++ b/backend/rorapp/functions/__init__.py
@@ -1,5 +1,6 @@
# The functions module provides a collection of public functions that are intended to be used by other parts of the application.
from .faction_leader_helper import select_faction_leader, set_faction_leader # noqa: F401
+from .forum_phase_helper import initiate_situation # noqa: F401
from .forum_phase_starter import start_forum_phase # noqa: F401
from .game_deleter import delete_all_games # noqa: F401
from .game_generator import generate_game # noqa: F401
diff --git a/backend/rorapp/functions/faction_leader_helper.py b/backend/rorapp/functions/faction_leader_helper.py
index b41ba9c7..8267a53b 100644
--- a/backend/rorapp/functions/faction_leader_helper.py
+++ b/backend/rorapp/functions/faction_leader_helper.py
@@ -2,6 +2,7 @@
from typing import Optional
from rorapp.functions.action_helper import delete_old_actions
from rorapp.functions.forum_phase_helper import (
+ generate_initiate_situation_action,
get_next_faction_in_forum_phase,
)
from rorapp.functions.mortality_phase_starter import setup_mortality_phase
@@ -23,7 +24,6 @@
from rorapp.serializers import (
ActionLogSerializer,
ActionSerializer,
- StepSerializer,
TitleSerializer,
SenatorActionLogSerializer,
)
@@ -87,7 +87,9 @@ def set_faction_leader(senator_id: int) -> (Response, dict):
messages_to_send.extend(proceed_to_next_step_if_forum_phase(game.id, step, faction))
messages_to_send.extend(delete_old_actions(game.id))
- return Response({"message": "Faction leader selected"}, status=200), messages_to_send
+ return Response(
+ {"message": "Faction leader selected"}, status=200
+ ), messages_to_send
def get_previous_title(faction) -> Optional[Title]:
@@ -182,30 +184,7 @@ def proceed_to_next_step_if_forum_phase(game_id, step, faction) -> [dict]:
next_faction = get_next_faction_in_forum_phase(faction)
if next_faction is not None:
- new_step = Step(index=step.index + 1, phase=step.phase)
- new_step.save()
-
- messages_to_send.append(
- generate_select_faction_leader_action(next_faction, new_step)
- )
-
- messages_to_send.append(
- create_websocket_message("step", StepSerializer(new_step).data)
- )
+ messages_to_send.extend(generate_initiate_situation_action(next_faction))
else:
messages_to_send.extend(start_next_turn(game_id, step))
return messages_to_send
-
-
-def generate_select_faction_leader_action(faction: Faction, step: Step) -> dict:
- senators = Senator.objects.filter(faction=faction, death_step__isnull=True)
- senator_id_list = [senator.id for senator in senators]
- action = Action(
- step=step,
- faction=faction,
- type="select_faction_leader",
- required=True,
- parameters=senator_id_list,
- )
- action.save()
- return create_websocket_message("action", ActionSerializer(action).data)
diff --git a/backend/rorapp/functions/forum_phase_helper.py b/backend/rorapp/functions/forum_phase_helper.py
index 76c09943..91d2f271 100644
--- a/backend/rorapp/functions/forum_phase_helper.py
+++ b/backend/rorapp/functions/forum_phase_helper.py
@@ -1,5 +1,17 @@
-from typing import Optional
-from rorapp.models import Faction
+from rest_framework.response import Response
+from typing import List, Optional
+from rorapp.functions.websocket_message_helper import (
+ create_websocket_message,
+ destroy_websocket_message,
+)
+from rorapp.models import (
+ Action,
+ Faction,
+ Phase,
+ Senator,
+ Step,
+)
+from rorapp.serializers import ActionSerializer, StepSerializer
def get_next_faction_in_forum_phase(
@@ -23,3 +35,81 @@ def get_next_faction_in_forum_phase(
if next_faction_index >= len(factions):
return None
return factions[next_faction_index]
+
+
+def generate_select_faction_leader_action(faction: Faction, step: Step) -> dict:
+ senators = Senator.objects.filter(faction=faction, death_step__isnull=True)
+ senator_id_list = [senator.id for senator in senators]
+ action = Action(
+ step=step,
+ faction=faction,
+ type="select_faction_leader",
+ required=True,
+ parameters=senator_id_list,
+ )
+ action.save()
+ return create_websocket_message("action", ActionSerializer(action).data)
+
+
+def generate_initiate_situation_action(faction: Faction) -> List[dict]:
+ messages_to_send = []
+
+ # Create new step
+ latest_step = Step.objects.filter(phase__turn__game=faction.game.id).order_by(
+ "-index"
+ )[0]
+ # Need to get latest phase because the latest step might not be from the current forum phase
+ latest_phase = Phase.objects.filter(turn__game=faction.game.id).order_by("-index")[
+ 0
+ ]
+ new_step = Step(index=latest_step.index + 1, phase=latest_phase)
+ new_step.save()
+ messages_to_send.append(
+ create_websocket_message("step", StepSerializer(new_step).data)
+ )
+
+ # Create new action
+ action = Action(
+ step=new_step,
+ faction=faction,
+ type="initiate_situation",
+ required=True,
+ )
+ action.save()
+ messages_to_send.append(
+ create_websocket_message("action", ActionSerializer(action).data)
+ )
+ return messages_to_send
+
+
+def initiate_situation(action_id: int) -> dict:
+ """
+ Initiate a random situation.
+
+ This function is called when a player initiates a situation during the forum phase.
+
+ Args:
+ action_id (int): The action ID.
+
+ Returns:
+ dict: The response with a message and a status code.
+ """
+ messages_to_send = []
+
+ # Mark the action as complete
+ action = Action.objects.get(id=action_id)
+ action.completed = True
+ action.save()
+ messages_to_send.append(destroy_websocket_message("action", action_id))
+
+ # Create new step
+ new_step = Step(index=action.step.index + 1, phase=action.step.phase)
+ new_step.save()
+ messages_to_send.append(
+ create_websocket_message("step", StepSerializer(new_step).data)
+ )
+
+ messages_to_send.append(
+ generate_select_faction_leader_action(action.faction, new_step)
+ )
+ return Response({"message": "Situation initiated"}, status=200), messages_to_send
diff --git a/backend/rorapp/functions/forum_phase_starter.py b/backend/rorapp/functions/forum_phase_starter.py
index 87d5a1e0..7c7721b9 100644
--- a/backend/rorapp/functions/forum_phase_starter.py
+++ b/backend/rorapp/functions/forum_phase_starter.py
@@ -1,19 +1,25 @@
from typing import List
from rorapp.functions.action_helper import delete_old_actions
-from rorapp.functions.faction_leader_helper import generate_select_faction_leader_action
+from rorapp.functions.forum_phase_helper import generate_initiate_situation_action
from rorapp.functions.websocket_message_helper import create_websocket_message
from rorapp.models import (
Faction,
Phase,
Step,
)
-from rorapp.serializers import (
- StepSerializer,
- PhaseSerializer,
-)
+from rorapp.serializers import PhaseSerializer
+
+def start_forum_phase(game_id: int) -> List[dict]:
+ """
+ Start the forum phase.
-def start_forum_phase(game_id) -> List[dict]:
+ Args:
+ game_id (int): The game ID.
+
+ Returns:
+ List[dict]: The WebSocket messages to send.
+ """
messages_to_send = []
latest_step = Step.objects.filter(phase__turn__game=game_id).order_by("-index")[0]
@@ -25,16 +31,9 @@ def start_forum_phase(game_id) -> List[dict]:
messages_to_send.append(
create_websocket_message("phase", PhaseSerializer(new_phase).data)
)
- new_step = Step(index=latest_step.index + 1, phase=new_phase)
- new_step.save()
- messages_to_send.append(
- create_websocket_message("step", StepSerializer(new_step).data)
- )
- # Create actions
+ # Create action and new step
first_faction = Faction.objects.filter(game__id=game_id).order_by("rank").first()
- messages_to_send.append(
- generate_select_faction_leader_action(first_faction, new_step)
- )
+ messages_to_send.extend(generate_initiate_situation_action(first_faction))
messages_to_send.extend(delete_old_actions(game_id))
return messages_to_send
diff --git a/backend/rorapp/functions/game_starter.py b/backend/rorapp/functions/game_starter.py
index 5ea60b33..622041d4 100644
--- a/backend/rorapp/functions/game_starter.py
+++ b/backend/rorapp/functions/game_starter.py
@@ -9,7 +9,7 @@
from django.contrib.auth.models import User
from rest_framework.response import Response
from rest_framework.exceptions import NotFound, PermissionDenied
-from rorapp.functions.faction_leader_helper import generate_select_faction_leader_action
+from rorapp.functions.forum_phase_helper import generate_select_faction_leader_action
from rorapp.functions.rank_helper import rank_senators_and_factions
from rorapp.functions.websocket_message_helper import (
send_websocket_messages,
diff --git a/backend/rorapp/tests/forum_phase_tests.py b/backend/rorapp/tests/forum_phase_tests.py
index e1e70d4d..01bda060 100644
--- a/backend/rorapp/tests/forum_phase_tests.py
+++ b/backend/rorapp/tests/forum_phase_tests.py
@@ -25,7 +25,7 @@ def test_forum_phase(self) -> None:
for player_count in range(3, 7):
self.do_forum_phase_test(player_count)
- def action_processor(self, action: Action) -> dict:
+ def faction_leader_action_processor(self, action: Action) -> dict:
faction = Faction.objects.filter(player=action.faction.player.id).get(
game=action.faction.game.id
)
@@ -35,32 +35,48 @@ def action_processor(self, action: Action) -> dict:
def do_forum_phase_test(self, player_count: int) -> None:
random.seed(1)
- game_id, faction_ids_with_leadership = self.setup_game_in_forum_phase(player_count)
+ game_id, faction_ids_with_leadership = self.setup_game_in_forum_phase(
+ player_count
+ )
for _ in range(0, player_count):
check_latest_phase(self, game_id, "Forum")
- potential_actions = get_and_check_actions(
+ situation_potential_actions = get_and_check_actions(
+ self, game_id, False, "initiate_situation", 1
+ )
+ submit_actions(
+ self,
+ game_id,
+ situation_potential_actions,
+ self.faction_leader_action_processor,
+ )
+ check_latest_phase(self, game_id, "Forum")
+ faction_leader_potential_actions = get_and_check_actions(
self, game_id, False, "select_faction_leader", 1
)
- self.assertEqual(len(potential_actions), 1)
faction_leader_titles = Title.objects.filter(
name="Faction Leader",
- senator__faction=potential_actions[0].faction,
- end_step=None
+ senator__faction=faction_leader_potential_actions[0].faction,
+ end_step=None,
)
-
+
# If the faction already has a leader, then there should be no existing faction leader title.
- self.assertEqual(len(faction_leader_titles), 1 if potential_actions[0].faction.id in faction_ids_with_leadership else 0)
+ self.assertEqual(
+ len(faction_leader_titles),
+ 1
+ if faction_leader_potential_actions[0].faction.id
+ in faction_ids_with_leadership
+ else 0,
+ )
submit_actions(
self,
game_id,
- potential_actions,
- self.action_processor,
+ faction_leader_potential_actions,
+ self.faction_leader_action_processor,
)
- self.assertEqual(len(potential_actions), 1)
faction_leader_titles = Title.objects.filter(
name="Faction Leader",
- senator__faction=potential_actions[0].faction,
- end_step=None
+ senator__faction=faction_leader_potential_actions[0].faction,
+ end_step=None,
)
self.assertEqual(len(faction_leader_titles), 1)
check_latest_phase(self, game_id, "Mortality")
@@ -72,7 +88,8 @@ def setup_game_in_forum_phase(self, player_count: int) -> (int, List[int]):
faction_ids_with_leadership = set_some_faction_leaders(game_id)
start_forum_phase(game_id)
return (game_id, faction_ids_with_leadership)
-
+
+
def set_some_faction_leaders(game_id: int) -> List[int]:
"""
Assigns faction leader titles to 2 senators then returns their faction IDs.
@@ -80,12 +97,15 @@ def set_some_faction_leaders(game_id: int) -> List[int]:
factions = Faction.objects.filter(game=game_id)
first_faction = factions.first()
second_faction = factions.last()
- senator_in_faction_1 = Senator.objects.filter(game=game_id, faction=first_faction).first()
- senator_in_faction_2 = Senator.objects.filter(game=game_id, faction=second_faction).first()
+ senator_in_faction_1 = Senator.objects.filter(
+ game=game_id, faction=first_faction
+ ).first()
+ senator_in_faction_2 = Senator.objects.filter(
+ game=game_id, faction=second_faction
+ ).first()
set_faction_leader(senator_in_faction_1.id)
set_faction_leader(senator_in_faction_2.id)
return [
senator_in_faction_1.faction.id,
senator_in_faction_2.faction.id,
]
-
diff --git a/backend/rorapp/tests/test_helper.py b/backend/rorapp/tests/test_helper.py
index c959266d..e293f8e8 100644
--- a/backend/rorapp/tests/test_helper.py
+++ b/backend/rorapp/tests/test_helper.py
@@ -1,10 +1,9 @@
from typing import List
from django.db.models.query import QuerySet
from django.test import TestCase
-from rorapp.models import Action, Phase, Senator, Step
+from rorapp.models import Action, Phase, Step, Turn
from django.contrib.auth.models import User
from typing import Callable
-from rorapp.functions import set_faction_leader
def check_all_actions(
@@ -118,8 +117,14 @@ def check_latest_phase(
expected_latest_phase_name: str,
expected_phase_count: int | None = None,
) -> None:
- phases = Phase.objects.filter(turn__game=game_id)
+ """
+ Check that the latest phase has the expected name, and matches the latest step and latest turn.
+ """
+ latest_turn = Turn.objects.filter(game=game_id).order_by("-index").first()
+ phases = Phase.objects.filter(turn=latest_turn).order_by("-index")
if expected_phase_count:
test_case.assertEqual(phases.count(), expected_phase_count)
- latest_phase = phases[len(phases) - 1]
+ latest_phase = phases.first()
+ latest_step = Step.objects.filter(phase__turn=latest_turn).order_by("-index").first()
+ test_case.assertEqual(latest_phase.id, latest_step.phase.id)
test_case.assertEqual(latest_phase.name, expected_latest_phase_name)
diff --git a/backend/rorapp/views/submit_action.py b/backend/rorapp/views/submit_action.py
index 38f6054c..c4b1ae2d 100644
--- a/backend/rorapp/views/submit_action.py
+++ b/backend/rorapp/views/submit_action.py
@@ -3,8 +3,12 @@
from rest_framework import viewsets
from rest_framework.decorators import action
from rest_framework.response import Response
-from rorapp.functions import face_mortality, select_faction_leader
-from rorapp.functions import send_websocket_messages
+from rorapp.functions import (
+ face_mortality,
+ initiate_situation,
+ select_faction_leader,
+ send_websocket_messages,
+)
from rorapp.models import Game, Faction, Step, Action
@@ -15,7 +19,9 @@ class SubmitActionViewSet(viewsets.ViewSet):
@action(detail=True, methods=["post"])
@transaction.atomic
- def submit_action(self, request: HttpRequest, game_id: int, action_id: int | None =None):
+ def submit_action(
+ self, request: HttpRequest, game_id: int, action_id: int | None = None
+ ):
# Try to get the game
try:
game = Game.objects.get(id=game_id)
@@ -55,7 +61,9 @@ def submit_action(self, request: HttpRequest, game_id: int, action_id: int | Non
return self.perform_action(game.id, action, request)
- def perform_action(self, game_id: int, action: Action, request: HttpRequest) -> Response:
+ def perform_action(
+ self, game_id: int, action: Action, request: HttpRequest
+ ) -> Response:
response = None
messages = None
match action.type:
@@ -63,6 +71,8 @@ def perform_action(self, game_id: int, action: Action, request: HttpRequest) ->
response, messages = select_faction_leader(action.id, request.data)
case "face_mortality":
response, messages = face_mortality(action.id)
+ case "initiate_situation":
+ response, messages = initiate_situation(action.id)
case _:
return Response({"message": "Action type is invalid"}, status=400)
diff --git a/frontend/components/GamePage.tsx b/frontend/components/GamePage.tsx
index 01724078..b1c82e19 100644
--- a/frontend/components/GamePage.tsx
+++ b/frontend/components/GamePage.tsx
@@ -11,7 +11,6 @@ import useWebSocket from "react-use-websocket"
import Tabs from "@mui/material/Tabs"
import Tab from "@mui/material/Tab"
-import Box from "@mui/material/Box"
import CircularProgress from "@mui/material/CircularProgress"
import { useGameContext } from "@/contexts/GameContext"
@@ -621,9 +620,7 @@ const GamePage = (props: GamePageProps) => {
{`${game!.name} | Republic of Rome Online`}
-
+
diff --git a/frontend/components/actionDialogs/ActionDialog.module.css b/frontend/components/actionDialogs/ActionDialog.module.css
deleted file mode 100644
index 1b58f5c5..00000000
--- a/frontend/components/actionDialogs/ActionDialog.module.css
+++ /dev/null
@@ -1,11 +0,0 @@
-button.closeButton {
- position: absolute;
- right: 8px;
- top: 8px;
-}
-
-.content {
- display: flex;
- flex-direction: column;
- gap: 16px;
-}
diff --git a/frontend/components/actionDialogs/ActionDialog.tsx b/frontend/components/actionDialogs/ActionDialog.tsx
index 16165f48..39bd4f8a 100644
--- a/frontend/components/actionDialogs/ActionDialog.tsx
+++ b/frontend/components/actionDialogs/ActionDialog.tsx
@@ -5,6 +5,7 @@ import Collection from "@/classes/Collection"
import Action from "@/classes/Action"
import SelectFactionLeaderDialog from "./ActionDialog_SelectFactionLeader"
import FaceMortalityDialog from "./ActionDialog_FaceMortality"
+import InitiateSituationDialog from "./ActionDialog_InitiateSituation"
interface ActionDialogProps {
actions: Collection
@@ -16,6 +17,7 @@ interface ActionDialogProps {
const dialogs: { [key: string]: React.ComponentType } = {
select_faction_leader: SelectFactionLeaderDialog,
face_mortality: FaceMortalityDialog,
+ initiate_situation: InitiateSituationDialog,
}
// Dialog box that displays the action that the player must take
diff --git a/frontend/components/actionDialogs/ActionDialog_FaceMortality.tsx b/frontend/components/actionDialogs/ActionDialog_FaceMortality.tsx
index 1da1d0e9..a6a44ff3 100644
--- a/frontend/components/actionDialogs/ActionDialog_FaceMortality.tsx
+++ b/frontend/components/actionDialogs/ActionDialog_FaceMortality.tsx
@@ -11,7 +11,6 @@ import CloseIcon from "@mui/icons-material/Close"
import Action from "@/classes/Action"
import Collection from "@/classes/Collection"
-import actionDialogStyles from "./ActionDialog.module.css"
import DeadIcon from "@/images/icons/dead.svg"
import request from "@/functions/request"
import { useAuthContext } from "@/contexts/AuthContext"
@@ -33,9 +32,7 @@ const FaceMortalityDialog = (props: FaceMortalityDialogProps) => {
} = useAuthContext()
const { game } = useGameContext()
- const [requiredAction, setRequiredAction] = useState(
- null
- )
+ const [requiredAction, setRequiredAction] = useState(null)
// Set required action
useEffect(() => {
@@ -64,15 +61,13 @@ const FaceMortalityDialog = (props: FaceMortalityDialogProps) => {
return (
<>
Ready to Face Mortality?
- props.setOpen(false)}
- >
-
-
+
+ props.setOpen(false)}>
+
+
+
-
+
“Death is the wish of some, the relief of many, and the end of all.”
Seneca the Younger
diff --git a/frontend/components/actionDialogs/ActionDialog_InitiateSituation.tsx b/frontend/components/actionDialogs/ActionDialog_InitiateSituation.tsx
new file mode 100644
index 00000000..472fb4f8
--- /dev/null
+++ b/frontend/components/actionDialogs/ActionDialog_InitiateSituation.tsx
@@ -0,0 +1,86 @@
+import { useEffect, useState } from "react"
+import Image from "next/image"
+import {
+ Button,
+ DialogActions,
+ DialogContent,
+ DialogTitle,
+ IconButton,
+} from "@mui/material"
+import CloseIcon from "@mui/icons-material/Close"
+
+import Action from "@/classes/Action"
+import Collection from "@/classes/Collection"
+import DeadIcon from "@/images/icons/dead.svg"
+import request from "@/functions/request"
+import { useAuthContext } from "@/contexts/AuthContext"
+import { useGameContext } from "@/contexts/GameContext"
+
+interface InitiateSituationDialogProps {
+ setOpen: (open: boolean) => void
+ actions: Collection
+}
+
+// Action dialog allows the player to ready up for mortality
+const InitiateSituationDialog = (props: InitiateSituationDialogProps) => {
+ const {
+ accessToken,
+ refreshToken,
+ setAccessToken,
+ setRefreshToken,
+ setUser,
+ } = useAuthContext()
+ const { game } = useGameContext()
+
+ const [requiredAction, setRequiredAction] = useState(null)
+
+ // Set required action
+ useEffect(() => {
+ const requiredAction = props.actions.asArray.find(
+ (a) => a.required === true
+ )
+ if (requiredAction) setRequiredAction(requiredAction)
+ }, [props.actions])
+
+ // Handle dialog submission
+ const handleSubmit = async () => {
+ if (game && requiredAction) {
+ request(
+ "POST",
+ `games/${game.id}/submit-action/${requiredAction.id}/`,
+ accessToken,
+ refreshToken,
+ setAccessToken,
+ setRefreshToken,
+ setUser
+ )
+ props.setOpen(false)
+ }
+ }
+
+ return (
+ <>
+ Initiate a Situation
+
+ props.setOpen(false)}>
+
+
+
+
+
+
+
+ You must initiate a random Situation. It could be a Secret, a Senator, an Event, a War or an Enemy Leader.
+
+
This feature is incomplete, so nothing actually happens.
+
+
+
+
+
+
+ >
+ )
+}
+
+export default InitiateSituationDialog
diff --git a/frontend/components/actionDialogs/ActionDialog_SelectFactionLeader.tsx b/frontend/components/actionDialogs/ActionDialog_SelectFactionLeader.tsx
index 3207132b..db2ba50c 100644
--- a/frontend/components/actionDialogs/ActionDialog_SelectFactionLeader.tsx
+++ b/frontend/components/actionDialogs/ActionDialog_SelectFactionLeader.tsx
@@ -1,4 +1,4 @@
-import { useEffect, useState } from "react"
+import { useState } from "react"
import {
Button,
DialogActions,
@@ -13,7 +13,6 @@ import { useGameContext } from "@/contexts/GameContext"
import Action from "@/classes/Action"
import Collection from "@/classes/Collection"
import Senator from "@/classes/Senator"
-import actionDialogStyles from "./ActionDialog.module.css"
import { useAuthContext } from "@/contexts/AuthContext"
import request from "@/functions/request"
@@ -80,15 +79,13 @@ const SelectFactionLeaderDialog = (props: SelectFactionLeaderDialogProps) => {
return (
<>
Select your Faction Leader
- props.setOpen(false)}
- >
-
-
+
+ props.setOpen(false)}>
+
+
+
-
+
Your Faction Leader will be immune from persuasion attempts. In the
unfortunate event of the death of your Faction Leader, his heir will
diff --git a/frontend/data/actions.json b/frontend/data/actions.json
index 6b30b8af..8cef0b61 100644
--- a/frontend/data/actions.json
+++ b/frontend/data/actions.json
@@ -6,5 +6,9 @@
"face_mortality": {
"sentence": "face mortality",
"title": "Face Mortality"
+ },
+ "initiate_situation": {
+ "sentence": "initiate a situation",
+ "title": "Initiate Situation"
}
}