Skip to content

Commit

Permalink
feature(demo-game): replace decision toggles with number input fields (
Browse files Browse the repository at this point in the history
  • Loading branch information
jajakob authored Jan 6, 2025
1 parent 78c2720 commit df119f1
Show file tree
Hide file tree
Showing 9 changed files with 335 additions and 1,089 deletions.
2 changes: 1 addition & 1 deletion apps/demo-game/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
"@radix-ui/react-dialog": "1.1.2",
"@radix-ui/react-popover": "1.1.2",
"@radix-ui/react-toast": "1.2.2",
"@uzh-bf/design-system": "3.0.0-alpha.13",
"@uzh-bf/design-system": "3.0.0-alpha.35",
"class-variance-authority": "0.7.0",
"clsx": "2.1.1",
"cmdk": "1.0.4",
Expand Down
21 changes: 15 additions & 6 deletions apps/demo-game/src/components/DecisionsDisplay.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,9 @@ interface DecisionProps {
period: ObjectProps
segment: ObjectProps
decisions: {
bank: boolean
bonds: boolean
stocks: boolean
bank: number
bonds: number
stocks: number
}
}

Expand Down Expand Up @@ -112,13 +112,22 @@ function DecisionsDisplayCompact({ segmentDecisions }: DecisionDisplayProps) {
P{e.period.index + 1} S{e.segment.index + 1}
</TableCell>
<TableCell>
<OnOffIcon on={e.decisions.bank} />
{/* <OnOffIcon on={e.decisions.bank} /> */}
<div className="flex justify-center">
{e.decisions.bank}%
</div>
</TableCell>
<TableCell>
<OnOffIcon on={e.decisions.bonds} />
{/* <OnOffIcon on={e.decisions.bonds} /> */}
<div className="flex justify-center">
{e.decisions.bonds}%
</div>
</TableCell>
<TableCell>
<OnOffIcon on={e.decisions.stocks} />
{/* <OnOffIcon on={e.decisions.stocks} /> */}
<div className="flex justify-center">
{e.decisions.stocks}%
</div>
</TableCell>
</TableRow>
)
Expand Down
6 changes: 3 additions & 3 deletions apps/demo-game/src/pages/admin/reports/[id].tsx
Original file line number Diff line number Diff line change
Expand Up @@ -579,9 +579,9 @@ function ReportGame() {
key={'decision-' + segmentIx}
className="flex w-40 items-center justify-around"
>
<div>{decision.bank}</div>
<div>{decision.bonds}</div>
<div>{decision.stocks}</div>
<div>{decision.bank} %</div>
<div>{decision.bonds} %</div>
<div>{decision.stocks} %</div>
</div>
)
})}
Expand Down
167 changes: 104 additions & 63 deletions apps/demo-game/src/pages/play/cockpit.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { useMutation, useQuery } from '@apollo/client'
import { Layout, PlayerDisplay, ProbabilityChart } from '@gbl-uzh/ui'
import { Switch } from '@uzh-bf/design-system'
import { Button, FormikNumberField, Switch } from '@uzh-bf/design-system'
import {
Card,
CardContent,
Expand Down Expand Up @@ -47,11 +47,12 @@ import {
UpdateReadyStateDocument,
} from 'src/graphql/generated/ops'
import { getSegmentEndResults } from 'src/lib/analysis'
import { ActionTypes } from 'src/services/ActionsReducer'
import { DecisionsDisplayCompact } from '~/components/DecisionsDisplay'
import LearningElements from '~/components/LearningElements'
import StoryElements from '~/components/StoryElements'
// TODO(JJ): This will be replaced by the design system
import { Form, Formik } from 'formik'
import * as yup from 'yup'
import { useToast } from '../../components/ui/use-toast'

const LABEL_MAP = {
Expand Down Expand Up @@ -813,39 +814,21 @@ function Cockpit() {
category: 'Savings',
currentValue: `${assets.bank.toFixed(2)} CHF`,
futureValue: `${(
assets.totalAssets *
(resultFactsDecisions.bank
? 1 /
(+resultFactsDecisions.bank +
+resultFactsDecisions.bonds +
+resultFactsDecisions.stocks)
: 0)
assets.totalAssets * resultFactsDecisions.bank
).toFixed(2)} CHF`,
},
{
category: 'Bonds',
currentValue: `${assets.bonds.toFixed(2)} CHF`,
futureValue: `${(
assets.totalAssets *
(resultFactsDecisions.bonds
? 1 /
(+resultFactsDecisions.bank +
+resultFactsDecisions.bonds +
+resultFactsDecisions.stocks)
: 0)
assets.totalAssets * resultFactsDecisions.bonds
).toFixed(2)} CHF`,
},
{
category: 'Stocks',
currentValue: `${assets.stocks.toFixed(2)} CHF`,
futureValue: `${(
assets.totalAssets *
(resultFactsDecisions.stocks
? 1 /
(+resultFactsDecisions.bank +
+resultFactsDecisions.bonds +
+resultFactsDecisions.stocks)
: 0)
assets.totalAssets * resultFactsDecisions.stocks
).toFixed(2)} CHF`,
},
{
Expand All @@ -858,27 +841,44 @@ function Cockpit() {
const decisions = [
{
name: 'Savings',
label: (percentage: number) =>
`Put ${(percentage * 100).toFixed()}% in savings.`,
state: resultFactsDecisions.bank,
action: ActionTypes.DECIDE_BANK,
},
{
name: 'Bonds',
label: (percentage: number) =>
`Invest ${(percentage * 100).toFixed()}% in bonds.`,
state: resultFactsDecisions.bonds,
action: ActionTypes.DECIDE_BONDS,
},
{
name: 'Stocks',
label: (percentage: number) =>
`Invest ${(percentage * 100).toFixed()}% in stocks.`,
state: resultFactsDecisions.stocks,
action: ActionTypes.DECIDE_STOCK,
},
]

const schema = yup
.object({
savings: yup
.number()
.integer()
.min(0, 'Savings % must be greater equal than 0')
.max(100, 'Savings % must be smaller equal than 100')
.required('Savings % is required'),
bonds: yup
.number()
.integer()
.min(0, 'Bonds % must be greater equal than 0')
.max(100, 'Bonds % must be smaller equal than 100')
.required('Bonds % is required'),
stocks: yup
.number()
.integer()
.min(0, 'Stocks % must be greater equal than 0')
.max(100, 'Stocks % must be smaller equal than 100')
.required('Stocks % is required'),
})
.test('sum', 'Sum of values must be 100', (values, ctx) => {
const sum = values.savings + values.bonds + values.stocks
if (sum === 100) return true
return ctx.createError({
path: 'sum',
message: 'Sum of values must be 100',
})
})
return (
<GameLayout>
<div className="flex w-full grid-cols-2 flex-col gap-4 xl:grid">
Expand Down Expand Up @@ -952,35 +952,76 @@ function Cockpit() {
</Card>

<div className="mt-8 flex flex-row gap-2">
{decisions.map((decision) => {
return (
<div className="p-1" key={decision.name}>
<Switch
label={decision.label(
decision.state
? 1 /
(+resultFactsDecisions.bank +
+resultFactsDecisions.bonds +
+resultFactsDecisions.stocks)
: 0
<Formik
initialValues={{
savings: resultFactsDecisions.bank,
bonds: resultFactsDecisions.bonds,
stocks: resultFactsDecisions.stocks,
}}
validationSchema={schema}
onSubmit={async (values) => {
const savings = parseInt(values.savings)
const bonds = parseInt(values.bonds)
const stocks = parseInt(values.stocks)

await performAction({
variables: {
type: '',
payload: JSON.stringify({
bank: savings,
bonds,
stocks,
}),
},
refetchQueries: [ResultDocument],
})
}}
>
{(newDecisionForm) => {
return (
<Form>
<div className="mb-2 flex gap-2">
{decisions.map((decision) => {
const fieldName = decision.name.toLowerCase()
return (
<FormikNumberField
key={fieldName}
placeholder="0 %"
label={decision.name}
name={fieldName}
tooltip={
<p>
Determine how much of all assets you want
to invest in the {decision.name}. The
total should be equal to 100 percent.
</p>
}
required
data={{ cy: decision.name + '-cy' }}
className={{ label: 'pb-2 font-normal' }}
/>
)
})}
</div>
{newDecisionForm.errors.sum && (
<div className="text-red-500">
The sum of the input values must be{' '}
<span className="font-bold">100</span>!
</div>
)}
checked={decision.state}
id="switch"
onCheckedChange={async (checked) => {
await performAction({
variables: {
type: decision.action,
payload: JSON.stringify({
decision: checked,
}),
},
refetchQueries: [ResultDocument],
})
}}
/>
</div>
)
})}
<Button
type="submit"
disabled={
!newDecisionForm.isValid ||
newDecisionForm.isSubmitting
}
>
Submit
</Button>
</Form>
)
}}
</Formik>
</div>
</CardContent>
<CardFooter className="text-slate-500">
Expand Down
82 changes: 16 additions & 66 deletions apps/demo-game/src/services/ActionsReducer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,35 +2,24 @@ import { Action } from '@gbl-uzh/platform'
import { debugLog } from '@gbl-uzh/platform/dist/lib/util'
import { PrismaClient } from '@prisma/client'
import { produce } from 'immer'
import { P, match } from 'ts-pattern'
import { Decisions } from '../types/facts'
import { PeriodFacts, PeriodSegmentFacts } from '../types/index'

export enum ActionTypes {
DECIDE_BANK = 'DECIDE_BANK',
DECIDE_BONDS = 'DECIDE_BONDS',
DECIDE_STOCK = 'DECIDE_STOCK',
NONE = '',
}

type PayloadType = {
playerArgs: {
decision: boolean
}
playerArgs: Decisions
segmentFacts: PeriodSegmentFacts
periodFacts: PeriodFacts
}

type State = {
decisions: {
bank: boolean
bonds: boolean
stocks: boolean
}
decisions: Decisions
}

type Actions =
| Action<ActionTypes.DECIDE_BANK, PayloadType, PrismaClient>
| Action<ActionTypes.DECIDE_BONDS, PayloadType, PrismaClient>
| Action<ActionTypes.DECIDE_STOCK, PayloadType, PrismaClient>
type Actions = Action<ActionTypes.NONE, PayloadType, PrismaClient>

export function apply(state: State, action: Actions) {
// TODO: move this to platform? -> reducer should not have to care about isDirty and other non-user-logicstuff
Expand All @@ -42,56 +31,17 @@ export function apply(state: State, action: Actions) {
// TODO: the user reducer could just get the "draft" inside this function as first parameter
// TODO: and platform would do all code around it
const newState = produce(baseState, (draft) => {
match(action)
.with(
{ type: ActionTypes.DECIDE_BANK, payload: P.select() },
(payload) => {
// check if any of the other two decisions is set to true
// otherwise, do not allow to set bank to false
if (
!payload.playerArgs.decision &&
!draft.result.decisions.bonds &&
!draft.result.decisions.stocks
) {
return
}

draft.result.decisions.bank = payload.playerArgs.decision
}
)
.with(
{ type: ActionTypes.DECIDE_BONDS, payload: P.select() },
(payload) => {
// check if any of the other two decisions is set to true
// otherwise, do not allow to set bank to false
if (
!payload.playerArgs.decision &&
!draft.result.decisions.bank &&
!draft.result.decisions.stocks
) {
return
}

draft.result.decisions.bonds = payload.playerArgs.decision
}
)
.with(
{ type: ActionTypes.DECIDE_STOCK, payload: P.select() },
(payload) => {
// check if any of the other two decisions is set to true
// otherwise, do not allow to set bank to false
if (
!payload.playerArgs.decision &&
!draft.result.decisions.bank &&
!draft.result.decisions.bonds
) {
return
}

draft.result.decisions.stocks = payload.playerArgs.decision
}
)
.exhaustive()
const { bank, bonds, stocks } = action.payload.playerArgs
if (bank < 0 || bank > 100)
throw new Error('Bank must be between 0 and 100')
if (bonds < 0 || bonds > 100)
throw new Error('Bonds must be between 0 and 100')
if (stocks < 0 || stocks > 100)
throw new Error('Stocks must be between 0 and 100')
if (bank + bonds + stocks !== 100)
throw new Error('Bank + Bonds + Stocks must equal 100')

draft.result.decisions = action.payload.playerArgs
})

// this computes the isDirty flag based on whether there were changes in state from baseState to newState
Expand Down
Loading

0 comments on commit df119f1

Please sign in to comment.