Skip to content
This repository has been archived by the owner on Aug 7, 2024. It is now read-only.

fix: Fix badges disappearing randomly #1671

Merged
merged 12 commits into from
Jun 16, 2022
234 changes: 116 additions & 118 deletions Pipfile.lock

Large diffs are not rendered by default.

18 changes: 9 additions & 9 deletions game_frontend/src/components/Badge/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,16 +29,16 @@ const BadgeModalImg = styled(Box)`
export default class BadgeModal extends Component {
static propTypes = {
modalOpen: PropTypes.bool,
taskId: PropTypes.string,
badgeId: PropTypes.string,
}

render() {
if (!this.props.modalOpen) {
return null
}

const taskId = this.props.taskId
const info = badgeInfo[taskId]
const badgeId = this.props.badgeId
const info = badgeInfo[badgeId]
if (info === undefined) {
return null
}
Expand All @@ -62,15 +62,15 @@ export default class BadgeModal extends Component {
}
}

export function getBadges(tasks) {
return tasks.map((task) => (
export function getBadges(badges) {
return badges.map((badge) => (
<Box
component="img"
style={{ height: 45, marginRight: 15 }}
alt={badgeInfo[task].name}
title={badgeInfo[task].name}
src={badgeInfo[task].img}
key={task}
alt={badgeInfo[badge].name}
title={badgeInfo[badge].name}
src={badgeInfo[badge].img}
key={badge}
/>
))
}
41 changes: 27 additions & 14 deletions game_frontend/src/components/NavigationBar/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,11 +38,12 @@ export class NavigationBar extends Component {
static propTypes = {
// the props received from redux state or reducers
modalOpen: PropTypes.bool,
completedTasks: PropTypes.string,
completedBadges: PropTypes.string,
badgesInit: PropTypes.func,
gameState: PropTypes.any,
}

state = { modalOpen: false, completedTasks: [], lastTask: '' }
state = { modalOpen: false, completedBadges: [], lastBadge: '' }

componentDidMount() {
this.props.badgesInit()
Expand All @@ -56,27 +57,38 @@ export class NavigationBar extends Component {

static getDerivedStateFromProps(props, state) {
// Any time completedTasks change, pass the new info as state
if (props.completedTasks !== undefined) {
// convert to string for comparison
const stateTasksString = state.completedTasks.join() + ','
if (props.completedBadges !== undefined) {
const worksheetID = props.gameState.worksheetID
let badges = props.completedBadges.split(',')
badges = badges.filter((s) => s) // remove empty element
// remove any badge that's not relevant to the current worksheet
badges = badges.filter((b) => {
return b.startsWith(worksheetID + ':')
})

if (props.completedTasks !== stateTasksString) {
let newTasks = props.completedTasks.split(',')
newTasks = newTasks.filter((s) => s) // remove empty element
const lastTask = newTasks[newTasks.length - 1] // assume the last element is the last task
// convert to string for comparison
const stateBadgesString = state.completedBadges.join() + ','

if (props.completedBadges !== stateBadgesString) {
const lastBadge = badges[badges.length - 1] // assume the last element is the last badge
// return popup with new badge
return {
modalOpen: props.modalOpen,
completedTasks: newTasks,
lastTask: lastTask,
completedBadges: badges,
lastBadge: lastBadge,
}
}
// return current badges
return {
modalOpen: false,
completedBadges: badges,
}
}
return null // no change
}

renderLogoToolbar = () => {
const badges = getBadges(this.state.completedTasks)
const badges = getBadges(this.state.completedBadges)
return (
<LogoToolbar>
<IconButton href={urlForAimmoDashboard} aria-label="Kurono dashboard" color="inherit">
Expand Down Expand Up @@ -109,15 +121,16 @@ export class NavigationBar extends Component {
{this.renderLogoToolbar()}
{this.renderButtonToolbar()}
</KuronoAppBar>
<BadgeModal taskId={this.state.lastTask} modalOpen={this.state.modalOpen} />
<BadgeModal badgeId={this.state.lastBadge} modalOpen={this.state.modalOpen} />
</NavigationBarLayout>
)
}
}

const mapStateToProps = (state) => ({
completedTasks: state.avatarWorker.completedTasks,
completedBadges: state.avatarWorker.completedBadges,
modalOpen: state.avatarWorker.modalOpen,
gameState: state.game.gameState,
})

const mapDispatchToProps = {
Expand Down
28 changes: 0 additions & 28 deletions game_frontend/src/pyodide/badges.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -121,32 +121,4 @@ def next_turn(world_state, avatar_state):

expect(result).toBe(expected)
})

it('filters badges', () => {
const badges = '1:1,1:2,1:3,2:1,2:2,3:1'

let gameState = {worksheetID: 1}

let result = filterByWorksheet(badges, gameState)

let expected = "1:1,1:2,1:3"

expect(result).toBe(expected)

gameState = {worksheetID: 2}

result = filterByWorksheet(badges, gameState)

expected = "2:1,2:2"

expect(result).toBe(expected)

gameState = {worksheetID: 3}

result = filterByWorksheet(badges, gameState)

expected = "3:1"

expect(result).toBe(expected)
})
})
36 changes: 6 additions & 30 deletions game_frontend/src/pyodide/badges.ts
Original file line number Diff line number Diff line change
@@ -1,25 +1,11 @@
/* eslint-env worker */
import ComputedTurnResult from './computedTurnResult'

export function filterByWorksheet(badges: any, gameState: any): string {
const worksheetID = gameState.worksheetID
let badgesArr = badges.split(',')

badgesArr = badgesArr.filter((s) => s) // remove empty element
// remove any badge that's not relevant to the current worksheet
badgesArr = badgesArr.filter((b) => {
return b.startsWith(worksheetID + ':')
})

return badgesArr.join(',')
}

export function checkIfBadgeEarned(
badges: string,
result: ComputedTurnResult,
userCode: string,
gameState: any,
playerAvatarId: number
gameState: any
): string {
const userPythonCode = userCode.replace(/\s*#.*/gm, '') // Remove all comment lines from the user's code
const badgesPerWorksheet = [
Expand All @@ -28,7 +14,7 @@ export function checkIfBadgeEarned(
{
id: 3,
worksheetID: 1,
trigger: badge3Trigger(result, userPythonCode, gameState, playerAvatarId),
trigger: badge3Trigger(result, userPythonCode),
},
]

Expand All @@ -40,7 +26,7 @@ export function checkIfBadgeEarned(
badge.trigger
) {
// Here is when a new badge is earned
// TODO on worksshet 2: This might have to order the badges, in case user does not do the worksheet in order
// TODO on worksheet 2: This might have to order the badges, in case user does not do the worksheet in order
badges += `${badgeWorksheetPair},`
}
}
Expand Down Expand Up @@ -71,21 +57,11 @@ function badge2Trigger(userPythonCode: string): boolean {
return substrings.every((substring) => userPythonCode.includes(substring))
}

function badge3Trigger(
result: any,
userPythonCode: string,
gameState: any,
playerAvatarId: number
): boolean {
function badge3Trigger(result: any, userPythonCode: string): boolean {
// Check the code contains certain keywords about moving to a cell
const substrings = ['world_state.can_move_to(', 'print(', 'if ']
const codeContainsKeywords = substrings.every((substring) => userPythonCode.includes(substring))

if (!codeContainsKeywords) return false

// Check action is move action
const isMoveAction = result.action.action_type === 'move'
if (!isMoveAction) return false

return true
// And check it returns a move action
return codeContainsKeywords && result.action.action_type === 'move';
}
9 changes: 2 additions & 7 deletions game_frontend/src/pyodide/pyodideRunner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,18 +16,13 @@ async function initializePyodideWorker() {
await worker.initializePyodide()
}

export async function filterByWorksheet(badges: string, gameState: any): Promise<string> {
return worker.filterByWorksheet(badges, gameState)
}

export async function checkIfBadgeEarned(
badges: string,
result: ComputedTurnResult,
userCode: string,
gameState: any,
playerAvatarId: number
gameState: any
): Promise<string> {
return worker.checkIfBadgeEarned(badges, result, userCode, gameState, playerAvatarId)
return worker.checkIfBadgeEarned(badges, result, userCode, gameState)
}

export async function updateAvatarCode(
Expand Down
6 changes: 0 additions & 6 deletions game_frontend/src/redux/features/AvatarWorker/actions.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,6 @@ const getBadgesRequest = () => ({
type: types.GET_BADGES_REQUEST,
})

const filterBadges = (badges) => ({
type: types.FILTER_BADGES,
payload: badges,
})

const getBadgesReceived = (badges) => ({
type: types.GET_BADGES_SUCCESS,
payload: badges,
Expand Down Expand Up @@ -61,7 +56,6 @@ export default {
avatarCodeUpdated,
avatarsNextActionComputed,
badgesEarned,
filterBadges,
getBadgesRequest,
getBadgesReceived,
checkBadgesReceived,
Expand Down
34 changes: 6 additions & 28 deletions game_frontend/src/redux/features/AvatarWorker/epics.js
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ const getBadgesEpic = (action$, state$, { api }) =>
ofType(types.PYODIDE_INITIALIZED),
mergeMap((action) =>
api.get(`badges/${state$.value.game.connectionParameters.game_id}/`).pipe(
map((response) => actions.filterBadges(response.badges)),
map((response) => actions.getBadgesReceived(response.badges)),
catchError((error) =>
of({
type: types.GET_BADGES_FAILURE,
Expand All @@ -109,26 +109,6 @@ const getBadgesEpic = (action$, state$, { api }) =>
)
)

/**
* Filter the badges to return those that are from the game's worksheet.
* @returns a redux action that contains a string storing the user's earned badges of that worksheet.
*/
const filterBadgesEpic = (action$, state$, { pyodideRunner: { filterByWorksheet } }) =>
action$.pipe(
ofType(types.FILTER_BADGES),
switchMap(({ payload: badges }) =>
from(filterByWorksheet(badges, state$.value.game.gameState))
),
map((badges) => actions.getBadgesReceived(badges)),
catchError((error) =>
of({
type: types.BADGES_CHECKED_FAILURE,
payload: error,
error: true,
})
)
)

/**
* Whenever the avatar's code is updated, get the user's badges information.
* @returns a redux action that contains a string storing the user's earned badges information.
Expand Down Expand Up @@ -157,18 +137,17 @@ const checkBadgesEpic = (action$, state$, { api }) =>
*/
const checkBadgesEarnedEpic = (action$, state$, { pyodideRunner: { checkIfBadgeEarned } }) =>
action$.pipe(
ofType(types.BADGES_CHECKED_SUCCESS),
switchMap(({ payload: badges }) =>
ofType(types.AVATAR_CODE_UPDATED),
switchMap(({ payload: computedTurnResult }) =>
action$.pipe(
ofType(types.AVATAR_CODE_UPDATED),
switchMap(({ payload: computedTurnResult }) =>
ofType(types.BADGES_CHECKED_SUCCESS),
switchMap(({ payload: badges }) =>
from(
checkIfBadgeEarned(
badges,
computedTurnResult,
state$.value.editor.code.codeOnServer,
state$.value.game.gameState,
state$.value.game.connectionParameters.currentAvatarID
state$.value.game.gameState
)
)
),
Expand Down Expand Up @@ -214,5 +193,4 @@ export default {
checkBadgesEpic,
postBadgesEpic,
checkBadgesEarnedEpic,
filterBadgesEpic,
}
4 changes: 2 additions & 2 deletions game_frontend/src/redux/features/AvatarWorker/reducers.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,13 @@ const actionReducer = (state = { initialized: false }, action) => {
case types.GET_BADGES_SUCCESS:
return {
...state,
completedTasks: action.payload,
completedBadges: action.payload,
modalOpen: false,
}
case types.BADGES_EARNED:
return {
...state,
completedTasks: action.payload,
completedBadges: action.payload,
modalOpen: true,
}
default:
Expand Down
2 changes: 0 additions & 2 deletions game_frontend/src/redux/features/AvatarWorker/types.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ const AVATARS_NEXT_ACTION_COMPUTED = 'features/AvatarWorker/AVATARS_NEXT_ACTION_
const BADGES_EARNED = 'features/AvatarWorker/BADGES_EARNED'
const BADGES_CHECKED_FAILURE = 'features/AvatarWorker/BADGES_CHECKED_FAILURE'
const BADGES_CHECKED_SUCCESS = 'features/AvatarWorker/BADGES_CHECKED_SUCCESS'
const FILTER_BADGES = 'features/AvatarWorker/FILTER_BADGES'
const GET_BADGES_REQUEST = 'features/AvatarWorker/GET_CODE_REQUEST'
const GET_BADGES_SUCCESS = 'features/AvatarWorker/GET_CODE_SUCCESS'
const GET_BADGES_FAILURE = 'features/AvatarWorker/GET_CODE_FAILURE'
Expand All @@ -24,7 +23,6 @@ export default {
BADGES_EARNED,
BADGES_CHECKED_FAILURE,
BADGES_CHECKED_SUCCESS,
FILTER_BADGES,
GET_BADGES_REQUEST,
GET_BADGES_SUCCESS,
GET_BADGES_FAILURE,
Expand Down