Skip to content

Commit 62b368c

Browse files
authored
Create GET all maze actions endpoint (#48)
1 parent ddd7175 commit 62b368c

File tree

8 files changed

+128
-70
lines changed

8 files changed

+128
-70
lines changed

src/@types/hackthelab.d.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -222,6 +222,34 @@ declare module "hackthelab" {
222222
score: number;
223223
}
224224

225+
interface UserActions {
226+
userId: number;
227+
userName: string;
228+
actions: ACtion[];
229+
}
230+
231+
/**
232+
* @swagger
233+
* components:
234+
* schemas:
235+
* AllActionResponseItem:
236+
* type: object
237+
* properties:
238+
* userId:
239+
* type: string
240+
* userName:
241+
* type: string
242+
* actions:
243+
* type: array
244+
* items:
245+
* $ref: '#/components/schemas/Action'
246+
* score:
247+
* type: number
248+
*/
249+
interface AllActionResponseItem extends UserActions {
250+
score: number;
251+
}
252+
225253
/**
226254
* @swagger
227255
* components:

src/controllers/maze-controller/getActions.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { ActionsResponse, MazeRequest } from "hackthelab";
2-
import { MazeService } from "services";
2+
import { MazeService, ScoreService } from "services";
33
import { asyncHandler } from "utils";
44
import { rethrowOrCreateError } from "utils/create-error";
55
import { matchedData, param } from "utils/custom-validator";
@@ -48,7 +48,7 @@ const getActions = asyncHandler(async (req, res) => {
4848

4949
try {
5050
const actions = await MazeService.getActions(data.userId, maze.id);
51-
const score = MazeService.getScore(data.userId, maze, actions);
51+
const score = ScoreService.calculateScore(actions);
5252

5353
const response: ActionsResponse = {
5454
actions,
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import { AllActionResponseItem, MazeRequest } from "hackthelab";
2+
import { MazeService, ScoreService } from "services";
3+
import { asyncHandler } from "utils";
4+
import { rethrowOrCreateError } from "utils/create-error";
5+
6+
/**
7+
* @swagger
8+
* /v1/maze/{mazeId}/actions:
9+
* get:
10+
* tags: [Maze (ADMIN)]
11+
* summary: Returns recorded actions and a maze.
12+
* parameters:
13+
* - $ref: '#/components/parameters/MazeRequestPathBase'
14+
* responses:
15+
* 200:
16+
* description: Actions successful
17+
* content:
18+
* application/json:
19+
* schema:
20+
* type: array
21+
* items:
22+
* $ref: '#/components/schemas/AllActionResponseItem'
23+
* 400:
24+
* $ref: '#/components/responses/BadRequest'
25+
* 401:
26+
* $ref: '#/components/responses/Unauthorized'
27+
* 403:
28+
* $ref: '#/components/responses/Forbidden'
29+
* 500:
30+
* $ref: '#/components/responses/ServerError'
31+
*/
32+
const getAllActions = asyncHandler(async (req, res) => {
33+
const { maze } = req as MazeRequest;
34+
35+
try {
36+
const actions = await MazeService.getAllActions(maze.id);
37+
38+
const response: AllActionResponseItem[] = actions.map(user => ({
39+
...user,
40+
score: ScoreService.calculateScore(user.actions),
41+
}));
42+
43+
res.status(200).json(response);
44+
} catch (e) {
45+
console.error(e);
46+
throw rethrowOrCreateError(e, 500, "Server Error", "An error occurred while trying to fetch actions.");
47+
}
48+
});
49+
50+
export default getAllActions;

src/controllers/maze-controller/maze-controller.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { Router } from "express";
33
import { hasRole, mazePathSchema, resolveMaze, validate } from "middleware/interceptors";
44
import { Controller } from "../index";
55
import getActions, { actionsSchema } from "./getActions";
6+
import getAllActions from "./getAllActions";
67
import getMaze from "./getMaze";
78
import getMazes, { mazesSchema } from "./getMazes";
89
import putMaze, { mazeUpdateSchema } from "./putMaze";
@@ -23,6 +24,7 @@ export class MazeController implements Controller {
2324
const mazeMiddleware = [validate(mazePathSchema), resolveMaze];
2425

2526
router.put("/maze/:mazeId", hasRole(Role.Developer), validate(mazeUpdateSchema), mazeMiddleware, putMaze);
27+
router.get("/maze/:mazeId/actions", hasRole(Role.Admin), ...mazeMiddleware, getAllActions);
2628
router.get("/maze/:mazeId/actions/:userId", hasRole(Role.Admin), ...mazeMiddleware, validate(actionsSchema), getActions);
2729
router.get("/maze/:mazeId", hasRole(Role.Developer), ...mazeMiddleware, getMaze);
2830
router.get("/mazes", validate(mazesSchema), getMazes);

src/controllers/rat-controller/getActions.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { ActionsResponse, MazeRequest } from "hackthelab";
2-
import { MazeService } from "services";
2+
import { MazeService, ScoreService } from "services";
33
import { asyncHandler, rethrowOrCreateError } from "utils";
44

55
/**
@@ -31,7 +31,7 @@ const getActions = asyncHandler(async (req, res) => {
3131

3232
try {
3333
const actions = await MazeService.getActions(user.id, maze.id);
34-
const score = MazeService.getScore(user.id, maze, actions);
34+
const score = ScoreService.calculateScore(actions);
3535

3636
const response: ActionsResponse = {
3737
actions,

src/data/repository/action-repository.ts

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,27 @@
11
import { ActionType } from "@enums";
22
import { pgQuery } from "data/db";
3-
import { Action, Coordinate } from "hackthelab";
3+
import { Action, Coordinate, UserActions } from "hackthelab";
44

5-
export const getAll = (userId: number, mazeId: string): Promise<Action[]> => {
5+
export const getAllForUserMaze = (userId: number, mazeId: string): Promise<Action[]> => {
66
return pgQuery("SELECT * FROM actions WHERE user_id = $1 AND maze_id = $2 ORDER BY time_ts DESC", [userId, mazeId]);
77
};
88

9+
export const GetAllForMaze = (mazeId: string): Promise<UserActions[]> => {
10+
return pgQuery(
11+
`
12+
SELECT a.user_id,
13+
u.name AS user_name,
14+
JSON_AGG(a.* ORDER BY a.time_ts) AS actions
15+
FROM actions a
16+
JOIN users u
17+
ON a.user_id = u.id
18+
WHERE a.maze_id = $1
19+
GROUP BY a.user_id, u.name;
20+
`,
21+
[mazeId],
22+
);
23+
};
24+
925
export const deleteForUserMaze = async (userId: number, mazeId: string): Promise<void> => {
1026
await pgQuery("DELETE FROM actions WHERE user_id = $1 AND maze_id = $2", [userId, mazeId]);
1127
};

src/services/maze-service.ts

Lines changed: 7 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,18 @@
1-
import { ActionType, Environment } from "@enums";
1+
import { Environment } from "@enums";
22
import { ActionRepository, MazeRepository } from "data/repository";
3-
import { Action, AdminCell, AdminMaze, Coordinate, Maze, MazeDictionary } from "hackthelab";
3+
import { Action, AdminCell, AdminMaze, Coordinate, Maze, MazeDictionary, UserActions } from "hackthelab";
44

55
const mazeStore: { [key in Environment]: MazeDictionary } = {
66
[Environment.Competition]: {},
77
[Environment.Sandbox]: {},
88
};
99

1010
export const getActions = (userId: number, mazeId: string): Promise<Action[]> => {
11-
return ActionRepository.getAll(userId, mazeId);
11+
return ActionRepository.getAllForUserMaze(userId, mazeId);
12+
};
13+
14+
export const getAllActions = async (mazeId: string): Promise<UserActions[]> => {
15+
return ActionRepository.GetAllForMaze(mazeId);
1216
};
1317

1418
export const getMazes = async (): Promise<MazeDictionary> => {
@@ -63,53 +67,3 @@ export const setLocked = async (mazeId: string, locked: boolean): Promise<void>
6367
export const isLocked = async (mazeId: string): Promise<boolean> => {
6468
return (await getMazeById(mazeId)).locked;
6569
};
66-
67-
export const getScore = (userId: number, maze: Maze, actions: Action[]): number => {
68-
const EXIT_BONUS = 5000;
69-
const CHEESE_BONUS = 1000;
70-
const HARVEST_BONUS = 2500;
71-
const MOVE_PENALTY = 2;
72-
const ACTION_PENALTY = 1;
73-
74-
// Get the maze open spaces
75-
// const openSpaceCount = maze.cells.length - wallCount.length;
76-
77-
// Get stats based on the rat's actions
78-
const numOfActions = actions.length;
79-
80-
let numOfMoves = 0;
81-
let numOfCheeseEaten = 0;
82-
let numOfCheeseHarvested = 0;
83-
let didExit = false;
84-
85-
actions.forEach(action => {
86-
if (action.success) {
87-
switch (action.actionType) {
88-
case ActionType.Move:
89-
numOfMoves++;
90-
break;
91-
case ActionType.Eat:
92-
numOfCheeseEaten++;
93-
break;
94-
case ActionType.Exit:
95-
didExit = true;
96-
break;
97-
case ActionType.Drop:
98-
numOfCheeseHarvested++;
99-
break;
100-
}
101-
}
102-
});
103-
104-
// Calculate the score
105-
const exitBonus = didExit ? EXIT_BONUS : 0;
106-
const cheeseBonus = numOfCheeseEaten * CHEESE_BONUS;
107-
const harvestBonus = numOfCheeseHarvested * HARVEST_BONUS;
108-
const actionPenalty = numOfActions * ACTION_PENALTY;
109-
const movePenalty = numOfMoves * MOVE_PENALTY;
110-
111-
const netScore = exitBonus + cheeseBonus + harvestBonus - (actionPenalty + movePenalty);
112-
113-
// Add up everything, but don't let the score go below 0.
114-
return Math.max(0, netScore);
115-
};

src/services/score-service.ts

Lines changed: 19 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,22 @@
1-
import { ActionType, CellType, Environment, Role } from "@enums";
1+
import { ActionType, Environment, Role } from "@enums";
22
import { pgQuery } from "data/db";
33
import { UserRepository } from "data/repository";
4-
import { Action, Award, Maze, RankingResult, Score } from "hackthelab";
4+
import { Action, Award, RankingResult, Score } from "hackthelab";
55
import { MazeService, ScoreService } from "services";
66

7-
export const getScore = (userId: number, maze: Maze, actions: Action[]): number => {
8-
const MOVE_EFFICIENCY_BONUS = 2500;
7+
export const calculateScore = (actions: Action[]): number => {
98
const EXIT_BONUS = 5000;
109
const CHEESE_BONUS = 1000;
10+
const HARVEST_BONUS = 2500;
11+
const MOVE_PENALTY = 2;
1112
const ACTION_PENALTY = 1;
1213

13-
// Get the maze open spaces
14-
const wallCount = maze.cells.filter(cell => cell.type === CellType.Wall);
15-
const openSpaceCount = maze.cells.length - wallCount.length;
16-
1714
// Get stats based on the rat's actions
1815
const numOfActions = actions.length;
1916

2017
let numOfMoves = 0;
2118
let numOfCheeseEaten = 0;
19+
let numOfCheeseHarvested = 0;
2220
let didExit = false;
2321

2422
actions.forEach(action => {
@@ -33,18 +31,28 @@ export const getScore = (userId: number, maze: Maze, actions: Action[]): number
3331
case ActionType.Exit:
3432
didExit = true;
3533
break;
34+
case ActionType.Drop:
35+
numOfCheeseHarvested++;
36+
break;
3637
}
3738
}
3839
});
3940

4041
// Calculate the score
4142
const exitBonus = didExit ? EXIT_BONUS : 0;
42-
const moveEfficiencyBonus = didExit ? Math.max(0, ((openSpaceCount - numOfMoves) / openSpaceCount) * MOVE_EFFICIENCY_BONUS) : 0;
4343
const cheeseBonus = numOfCheeseEaten * CHEESE_BONUS;
44+
const harvestBonus = numOfCheeseHarvested * HARVEST_BONUS;
4445
const actionPenalty = numOfActions * ACTION_PENALTY;
46+
const movePenalty = numOfMoves * MOVE_PENALTY;
47+
48+
const bonuses = exitBonus + cheeseBonus + harvestBonus;
49+
const penalties = actionPenalty + movePenalty;
50+
51+
// Calculate the net score
52+
const netScore = bonuses - penalties;
4553

4654
// Add up everything, but don't let the score go below 0.
47-
return Math.floor(Math.max(0, exitBonus + moveEfficiencyBonus + cheeseBonus - actionPenalty));
55+
return Math.max(0, netScore);
4856
};
4957

5058
// Returns the rankings for all participants for the given maze ids
@@ -72,7 +80,7 @@ export const getRankings = async (environment: Environment): Promise<RankingResu
7280
}
7381

7482
const actions = await MazeService.getActions(userId, mazeId);
75-
const score = ScoreService.getScore(userId, mazes[mazeId], actions);
83+
const score = ScoreService.calculateScore(actions);
7684

7785
totalScore += score;
7886
}

0 commit comments

Comments
 (0)