Skip to content

Commit

Permalink
Finish up collective cubing feature
Browse files Browse the repository at this point in the history
  • Loading branch information
dmint789 committed May 1, 2024
1 parent 0120592 commit 671bb19
Show file tree
Hide file tree
Showing 6 changed files with 157 additions and 102 deletions.
206 changes: 115 additions & 91 deletions client/app/components/CollectiveCubing.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,12 @@

import { useEffect, useState } from 'react';
import { TwistyPlayer } from 'cubing/twisty';
import { keyToMove } from 'cubing/alg';
import myFetch, { FetchObj } from '~/helpers/myFetch';
import Button from '@c/UI/Button';
import { IFeCollectiveSolution, IMakeMoveDto, NxNMove } from '@sh/types';

const cubeMoves: [NxNMove[], NxNMove[], NxNMove[]] = [
['U', 'L', 'F', 'R', 'B', 'D'],
["U'", "L'", "F'", "R'", "B'", "D'"],
['U2', 'L2', 'F2', 'R2', 'B2', 'D2'],
];
import { nxnMoves } from '@sh/types/NxNMove';
import { getIsWebglUnsupported } from '~/helpers/utilityFunctions';

const addTwistyPlayerElement = async (alg = '') => {
const twistyPlayerElements = document.getElementsByTagName('twisty-player');
Expand All @@ -31,8 +28,12 @@ const addTwistyPlayerElement = async (alg = '') => {
const getCubeState = (colSol: IFeCollectiveSolution): string => `${colSol.scramble} z2 ${colSol.solution}`.trim();

const CollectiveCubing = () => {
const isWebglUnsupported = getIsWebglUnsupported();

const [loadingId, setLoadingId] = useState('');
const [collectiveSolutionError, setCollectiveSolutionError] = useState('');
const [collectiveSolutionError, setCollectiveSolutionError] = useState(
isWebglUnsupported ? 'Please enable WebGL to render the cube' : '',
);
const [collectiveSolution, setCollectiveSolution] = useState<IFeCollectiveSolution>();
const [selectedMove, setSelectedMove] = useState<NxNMove | null>(null);

Expand All @@ -42,116 +43,139 @@ const CollectiveCubing = () => {
: 0;

useEffect(() => {
if (isWebglUnsupported) return;

const doMoveWithKeyboard = (e: KeyboardEvent) => {
const move = keyToMove(e)?.toString();
console.log(move);
// selectMoveWithKeyboard(move as NxNMove);
};

myFetch.get('/collective-solution').then(({ payload, errors }: FetchObj<IFeCollectiveSolution>) => {
if (errors) {
setCollectiveSolutionError(errors[0]);
} else if (payload) {
setCollectiveSolution(payload);
addTwistyPlayerElement(getCubeState(payload));
} else {
addTwistyPlayerElement();
if (payload) {
setCollectiveSolution(payload);
addTwistyPlayerElement(getCubeState(payload));
} else {
addTwistyPlayerElement();
}

addEventListener('keypress', doMoveWithKeyboard);
}
});

return () => removeEventListener('keypress', doMoveWithKeyboard);
}, []);

const update = ({ payload, errors, errorData }: FetchObj<IFeCollectiveSolution>) => {
const newCollectiveSolution = payload ?? errorData?.collectiveSolution;

if (errors) setCollectiveSolutionError(errors[0]);
else setCollectiveSolutionError('');

if (newCollectiveSolution) {
setCollectiveSolution(newCollectiveSolution);
addTwistyPlayerElement(getCubeState(newCollectiveSolution));
}

setSelectedMove(null);
setLoadingId('');
};

const scrambleCube = async () => {
setLoadingId('scramble_button');

const { payload, errors }: FetchObj<IFeCollectiveSolution> = await myFetch.post('/collective-solution', {});
const fetchData = await myFetch.post('/collective-solution', {});
update(fetchData);
};

if (errors) {
setCollectiveSolutionError(errors[0]);
setLoadingId('');
} else if (payload) {
setCollectiveSolution(payload);
addTwistyPlayerElement(getCubeState(payload));
setCollectiveSolutionError('');
setLoadingId('');
}
const selectMove = (move: NxNMove) => {
setSelectedMove(move);
document.getElementById('confirm_button')?.focus();
};

const confirmMove = async () => {
setLoadingId('confirm_button');

const makeMoveDto: IMakeMoveDto = { move: selectedMove, lastSeenSolution: collectiveSolution.solution };
const { payload, errors } = await myFetch.post('/collective-solution/make-move', makeMoveDto);

if (errors) {
setCollectiveSolutionError(errors[0]);
setLoadingId('');
} else if (payload) {
setCollectiveSolution(payload);
addTwistyPlayerElement(getCubeState(payload));
setCollectiveSolutionError('');
setLoadingId('');
}

setSelectedMove(null);
const fetchData = await myFetch.post('/collective-solution/make-move', makeMoveDto);
update(fetchData);
};

return (
<>
<p>
Let's solve Rubik's Cubes together! Simply log in and make a turn. U is the yellow face, F is the green face.
You may not make two turns in a row.
Let's solve Rubik's Cubes together! Simply log in and make a turn. U is the yellow face and F is green. You may
not make two turns in a row.
</p>

{collectiveSolutionError ? (
<p className="text-danger">{collectiveSolutionError}</p>
) : (
collectiveSolution && <p>Scramble: {collectiveSolution.scramble}</p>
)}

<div className="row">
<div className="col-4">
<div id="twisty_player_container"></div>
{isSolved && (
<Button
id="scramble_button"
text="Scramble"
onClick={scrambleCube}
loadingId={loadingId}
className="btn-success w-100 mt-2 mb-4"
/>
)}
<p>All-time number of solves: {numberOfSolves}</p>
</div>
<div className="col-8" style={{ maxWidth: '500px' }}>
{!isSolved && (
<>
{cubeMoves.map((row, index) => (
<div key={index} className="row my-3">
{row.map((move) => (
<div key={move} className="col">
<button
type="button"
onClick={() => setSelectedMove(move)}
className={`btn btn-primary ${selectedMove === move ? 'active' : ''} w-100`}
>
{move}
</button>
</div>
))}
</div>
))}
<div className="row p-2">
<Button
id="confirm_button"
text="Confirm"
onClick={confirmMove}
disabled={!selectedMove}
loadingId={loadingId}
className="btn-success"
/>
{collectiveSolutionError && <p className="text-danger fw-bold">{collectiveSolutionError}</p>}

{!isWebglUnsupported && (
<>
{collectiveSolution && <p>Scramble: {collectiveSolution.scramble}</p>}

<div className="row gap-3">
<div className="col-md-4">
<div className="d-flex flex-column align-items-center">
<div id="twisty_player_container" style={{ maxWidth: '100%' }}></div>
{isSolved && (
<Button
id="scramble_button"
text="Scramble"
onClick={scrambleCube}
loadingId={loadingId}
className="btn-success w-100 mt-2 mb-4"
/>
)}
<p>
All-time number of solves: <b>{numberOfSolves}</b>
</p>
</div>
<div className="row p-3">
Moves used:{' '}
{collectiveSolution?.solution ? (collectiveSolution.solution.match(/ /g)?.length ?? 0) + 1 : 0}
</div>
</>
)}
</div>
</div>
</div>
<div className="col-md-8 " style={{ maxWidth: '500px' }}>
{!isSolved && (
<>
<div
className="gap-1 gap-md-3 mt-1 mt-md-4"
style={{ display: 'grid', gridTemplateColumns: 'repeat(6, 1fr)' }}
>
{nxnMoves.map((move) => (
<div key={move} className="p-0">
<button
type="button"
onClick={() => selectMove(move)}
className={`btn btn-primary ${selectedMove === move ? 'active' : ''} w-100`}
>
{move}
</button>
</div>
))}
</div>
<div className="my-3 my--md-4">
<Button
id="confirm_button"
text="Confirm"
onClick={confirmMove}
disabled={!selectedMove}
loadingId={loadingId}
className="btn-success w-100"
/>
</div>
<p className="my-2">
Moves used:{' '}
<b>
{collectiveSolution?.solution ? (collectiveSolution.solution.match(/ /g)?.length ?? 0) + 1 : 0}
</b>
</p>
</>
)}
</div>
</div>
</>
)}
</>
);
};
Expand Down
7 changes: 3 additions & 4 deletions client/app/mod/competition/ContestForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,6 @@ import {
ICutoff,
ITimeLimit,
IContestData,
IContest,
} from '@sh/types';
import {
Color,
Expand Down Expand Up @@ -131,10 +130,10 @@ const ContestForm = ({
const newFiltEv = isAdmin
? events
: events.filter(
(ev) =>
!ev.groups.some((g) => [EventGroup.ExtremeBLD, EventGroup.Removed].includes(g)) &&
(ev) =>
!ev.groups.some((g) => [EventGroup.ExtremeBLD, EventGroup.Removed].includes(g)) &&
(type !== ContestType.WcaComp || !ev.groups.includes(EventGroup.WCA)),
);
);

// Reset new event ID if new filtered events don't include it
if (newFiltEv.length > 0 && !newFiltEv.some((ev) => ev.eventId === newEventId))
Expand Down
7 changes: 5 additions & 2 deletions client/helpers/myFetch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ type HttpMethod = 'GET' | 'POST' | 'PATCH' | 'DELETE';
export type FetchObj<T = any> = {
payload?: T;
errors?: string[];
errorData?: any;
};

const API_BASE_URL =
Expand Down Expand Up @@ -94,20 +95,22 @@ const doFetch = async (
return {};
} else {
let errors: string[];
let errorData: any;

if (json?.message) {
// Sometimes the server returns the message as a single string and sometimes as an array of messages
if (typeof json.message === 'string') errors = [json.message];
else errors = json.message;

errors = errors.filter((err) => err !== '');
errors = errors.filter((err) => err.trim() !== '');
errorData = json.data;
} else if (res.status === 404 || is404) {
errors = [`Not found: ${url}`];
} else {
errors = ['Unknown error'];
}

return { errors };
return { errors, errorData };
}
} else if (!fileName && json) {
return { payload: json };
Expand Down
12 changes: 12 additions & 0 deletions client/helpers/utilityFunctions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -443,3 +443,15 @@ export const logOutUser = () => {
localStorage.removeItem('jwtToken');
window.location.href = '/';
};

export const getIsWebglUnsupported = (): boolean => {
try {
const canvas = document.createElement('canvas');
const webglContext = canvas.getContext('webgl');
const webglExperimentalContext = canvas.getContext('experimental-webgl');

return !window.WebGLRenderingContext || !webglContext || !webglExperimentalContext;
} catch (e) {
return true;
}
};
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,14 @@ export class CollectiveSolutionService {
}

public async startNewSolution(user: IPartialUser): Promise<IFeCollectiveSolution> {
const alreadyScrambledSolution = await this.collectiveSolutionModel.findOne({ state: 10 }).exec();
if (alreadyScrambledSolution) {
throw new BadRequestException({
message: 'The cube has already been scrambled',
data: { collectiveSolution: this.mapCollectiveSolution(alreadyScrambledSolution) },
});
}

this.logger.logAndSave('Generating new Collective Cubing scramble.', LogType.StartNewSolution);

const { randomScrambleForEvent } = await importEsmModule('cubing/scramble');
Expand Down Expand Up @@ -64,17 +72,19 @@ export class CollectiveSolutionService {
throw new BadRequestException(message);
}

if (currentSolution.solution !== makeMoveDto.lastSeenSolution)
throw new BadRequestException('The state of the cube has changed before your move. Please reload and try again.');
if (currentSolution.solution !== makeMoveDto.lastSeenSolution) {
throw new BadRequestException({
message: 'The state of the cube has changed before your move',
data: { collectiveSolution: this.mapCollectiveSolution(currentSolution) },
});
}

currentSolution.solution = `${currentSolution.solution} ${makeMoveDto.move}`.trim();
currentSolution.lastUserWhoInteracted = new mongoose.Types.ObjectId(user._id as string);
if (!currentSolution.usersWhoMadeMoves.some((usr) => usr.toString() === user._id))
currentSolution.usersWhoMadeMoves.push(currentSolution.lastUserWhoInteracted);

if (await this.getIsSolved(currentSolution)) {
currentSolution.state = 20;
}
if (await this.getIsSolved(currentSolution)) currentSolution.state = 20;

await currentSolution.save();

Expand Down
7 changes: 7 additions & 0 deletions server/src/modules/contests/contests.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,13 @@ export class ContestsService {
// Check access rights
if (!user.roles.includes(Role.Admin)) {
const person = await this.personsService.getPersonById(user.personId);

if (!person) {
throw new BadRequestException(
'Your profile must be tied to your account before you can use moderator features',
);
}

queryFilter = { organizers: person._id };
}

Expand Down

0 comments on commit 671bb19

Please sign in to comment.