Skip to content

Commit

Permalink
Player start on exit (#1083)
Browse files Browse the repository at this point in the history
* wip

* seems to work

* improvements

* fixes

* even stricter logic for player tiles

---------

Co-authored-by: Spencer Spenst <spencerspenst@gmail.com>
  • Loading branch information
k2xl and sspenst authored Mar 7, 2024
1 parent a2f1e49 commit 0cada84
Show file tree
Hide file tree
Showing 14 changed files with 124 additions and 72 deletions.
34 changes: 27 additions & 7 deletions components/editor/index.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { GameType, ValidateLevelResponse } from '@root/constants/Games';
import { ValidateLevelResponse } from '@root/constants/Games';
import { AppContext } from '@root/contexts/appContext';
import TileTypeHelper from '@root/helpers/tileTypeHelper';
import { useRouter } from 'next/router';
Expand Down Expand Up @@ -176,6 +176,22 @@ export default function Editor({ isDirty, level, setIsDirty, setLevel }: EditorP
clear = true;
}

// handle all cases related to the player
if (prevTileType === TileType.Player || prevTileType === TileType.PlayerOnExit) {
// disallow all changes except for exit when it is allowed
if (tileType !== TileType.Exit || !game.allowMovableOnExit) {
return prevLevel;
}

const newTileType = clear ? TileType.Player : TileTypeHelper.getExitSibilingTileType(prevTileType);

level.data = level.data.substring(0, index) + newTileType + level.data.substring(index + 1);

historyPush(level);

return level;
}

function getNewTileType() {
if (game.allowMovableOnExit) {
// place movable on exit or replace movable on exit
Expand All @@ -199,11 +215,17 @@ export default function Editor({ isDirty, level, setIsDirty, setLevel }: EditorP
const newTileType = clear ? TileType.Default : getNewTileType();

// when changing start position the old position needs to be removed
if (newTileType === TileType.Player) {
const startIndex = level.data.indexOf(TileType.Player);
if (newTileType === TileType.Player || newTileType === TileType.PlayerOnExit) {
const playerIndex = level.data.indexOf(TileType.Player);

if (playerIndex !== -1) {
level.data = level.data.substring(0, playerIndex) + TileType.Default + level.data.substring(playerIndex + 1);
}

const playerOnExitIndex = level.data.indexOf(TileType.PlayerOnExit);

if (startIndex !== -1) {
level.data = level.data.substring(0, startIndex) + TileType.Default + level.data.substring(startIndex + 1);
if (playerOnExitIndex !== -1) {
level.data = level.data.substring(0, playerOnExitIndex) + TileType.Exit + level.data.substring(playerOnExitIndex + 1);
}
}

Expand Down Expand Up @@ -292,7 +314,6 @@ export default function Editor({ isDirty, level, setIsDirty, setLevel }: EditorP
return 'editor-selected';
}
}}
hideText={game.type === GameType.COMPLETE_AND_SHORTEST}
id='editor-selection'
level={editorSelectionLevel}
onClick={(index) => setTileType(editorSelectionLevel.data[index] as TileType)}
Expand Down Expand Up @@ -338,7 +359,6 @@ export default function Editor({ isDirty, level, setIsDirty, setLevel }: EditorP
),
]),
]}
hideText={game.type === GameType.COMPLETE_AND_SHORTEST}
id={level._id?.toString() ?? 'new'}
level={level}
onClick={onClick}
Expand Down
2 changes: 0 additions & 2 deletions components/home/tutorialPathology.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import { GameState } from '@root/helpers/gameStateHelpers';
import classNames from 'classnames';
import { Types } from 'mongoose';
import Link from 'next/link';
import { useRouter } from 'next/router';
import React, { useCallback, useContext, useEffect, useRef, useState } from 'react';
import { AppContext } from '../../contexts/appContext';
import { TimerUtil } from '../../helpers/getTs';
Expand Down Expand Up @@ -64,7 +63,6 @@ export default function TutorialPathology() {
} as Level;
}

const router = useRouter();
const globalTimeout = useRef<NodeJS.Timeout | null>(null);
const [isNextButtonDisabled, setIsNextButtonDisabled] = useState(false);
const [isPrevButtonDisabled, setIsPrevButtonDisabled] = useState(false);
Expand Down
43 changes: 3 additions & 40 deletions components/level/basicLayout.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
import TileType from '@root/constants/tileType';
import { BlockState, GameState, TileState } from '@root/helpers/gameStateHelpers';
import TileTypeHelper from '@root/helpers/tileTypeHelper';
import { initGameState } from '@root/helpers/gameStateHelpers';
import React from 'react';
import Control from '../../models/control';
import Level from '../../models/db/level';
Expand All @@ -18,50 +16,15 @@ interface BasicLayoutProps {
}

export default function BasicLayout({ cellClassName, cellStyle, controls, hideText, id, level, onClick }: BasicLayoutProps) {
const data = level.data.split('\n');
const height = data.length;
const width = data[0].length;
const board = Array(height).fill(undefined).map(() =>
new Array(width).fill(undefined).map(() => {
return {
block: undefined,
text: [],
tileType: TileType.Default,
} as TileState;
}));
let blockId = 0;

for (let y = 0; y < height; y++) {
for (let x = 0; x < width; x++) {
const tileType = data[y][x] as TileType;

if (tileType === TileType.Wall ||
tileType === TileType.Exit ||
tileType === TileType.Hole ||
tileType === TileType.Player) {
board[y][x].tileType = tileType;
} else if (TileTypeHelper.isOnExit(tileType)) {
board[y][x].tileType = TileType.Exit;
board[y][x].block = {
id: blockId++,
tileType: TileTypeHelper.getExitSibilingTileType(tileType),
} as BlockState;
} else if (TileTypeHelper.canMove(tileType)) {
board[y][x].block = {
id: blockId++,
tileType: tileType,
} as BlockState;
}
}
}
const gameState = initGameState(level.data);

return (
<>
<Grid
cellClassName={(x, y) => cellClassName ? cellClassName(y * (level.width + 1) + x) : undefined}
cellStyle={(x, y) => cellStyle ? cellStyle(y * (level.width + 1) + x) : undefined}
disableAnimation
gameState={{ board: board } as GameState}
gameState={gameState}
hideText={hideText}
id={id}
leastMoves={level.leastMoves}
Expand Down
17 changes: 9 additions & 8 deletions components/level/grid.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -87,14 +87,14 @@ export default function Grid({ cellClassName, cellStyle, disableAnimation, gameO
<Tile
className={cellClassName ? cellClassName(x, y) : undefined}
disableAnimation={disableAnimation}
game={game}
handleClick={onCellClick ? (rightClick: boolean) => onCellClick(x, y, rightClick) : undefined}
key={`tile-${y}-${x}`}
pos={new Position(x, y)}
style={cellStyle ? cellStyle(x, y) : undefined}
text={text}
tileType={tileType}
game={game}
theme={theme as Theme}
tileType={tileType}
visited={tileState.text.length > 0}
/>
);
Expand All @@ -107,14 +107,14 @@ export default function Grid({ cellClassName, cellStyle, disableAnimation, gameO
<Tile
className={cellClassName ? cellClassName(x, y) : undefined}
disableAnimation={disableAnimation}
game={game}
handleClick={onCellClick ? (rightClick: boolean) => onCellClick(x, y, rightClick) : undefined}
key={`block-${tileState.block.id}`}
onTopOf={tileAtPosition.tileType}
pos={new Position(x, y)}
style={cellStyle ? cellStyle(x, y) : undefined}
tileType={tileState.block.tileType}
game={game}
theme={theme as Theme}
tileType={tileState.block.tileType}
/>
);
}
Expand All @@ -124,14 +124,14 @@ export default function Grid({ cellClassName, cellStyle, disableAnimation, gameO
<Tile
className={cellClassName ? cellClassName(x, y) : undefined}
disableAnimation={disableAnimation}
game={game}
handleClick={onCellClick ? (rightClick: boolean) => onCellClick(x, y, rightClick) : undefined}
inHole={true}
key={`block-${tileState.blockInHole.id}`}
pos={new Position(x, y)}
style={cellStyle ? cellStyle(x, y) : undefined}
tileType={tileState.blockInHole.tileType}
game={game}
theme={theme as Theme}
tileType={tileState.blockInHole.tileType}
/>
);
}
Expand Down Expand Up @@ -196,13 +196,14 @@ export default function Grid({ cellClassName, cellStyle, disableAnimation, gameO
atEnd={game.isComplete(gameState)}
className={cellClassName ? cellClassName(gameState.pos.x, gameState.pos.y) : undefined}
disableAnimation={disableAnimation}
game={game}
handleClick={onCellClick ? (rightClick: boolean) => onCellClick(gameState.pos.x, gameState.pos.y, rightClick) : undefined}
onTopOf={gameState.board[gameState.pos.y][gameState.pos.x].tileType}
pos={gameState.pos}
style={cellStyle ? cellStyle(gameState.pos.x, gameState.pos.y) : undefined}
text={gameState.moves.length}
tileType={TileType.Player}
game={game}
theme={theme as Theme}
tileType={TileType.Player}
/>
}
</div>
Expand Down
1 change: 0 additions & 1 deletion components/level/tile/block.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,6 @@ export default function Block({ game, inHole, onTopOf, theme, tileType }: BlockP
'tile-type-' + tileType,
'tile-' + game.id,
inHole ? styles['in-hole'] : undefined,
{ 'on-exit': onTopOf === TileType.Exit },
)}
style={{
backgroundColor: getBackgroundColor(),
Expand Down
5 changes: 3 additions & 2 deletions components/level/tile/player.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,11 @@ interface PlayerProps {
atEnd?: boolean;
game: Game;
moveCount: number;
onTopOf?: TileType;
theme: Theme;
}

export default function Player({ atEnd, game, moveCount, theme }: PlayerProps) {
export default function Player({ atEnd, game, moveCount, onTopOf, theme }: PlayerProps) {
const { borderWidth, hideText, innerTileSize, leastMoves, tileSize } = useContext(GridContext);
const text = hideText ? '' : String(moveCount);
const fontSizeRatio = text.length <= 3 ? 2 : (1 + (text.length - 1) / 2);
Expand Down Expand Up @@ -41,7 +42,7 @@ export default function Player({ atEnd, game, moveCount, theme }: PlayerProps) {
boxShadow: classic ?
`-${2 * borderWidth}px ${2 * borderWidth}px 0 0 var(--bg-color)` :
`0 0 0 ${borderWidth}px var(--bg-color)`,
color: 'var(--level-player-text)',
color: onTopOf === TileType.Exit ? 'var(--level-end)' : 'var(--level-player-text)',
fontSize: fontSize,
height: innerTileSize,
left: classic ? 2 * borderWidth : 0,
Expand Down
1 change: 1 addition & 0 deletions components/level/tile/tile.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ export default function Tile({
atEnd={atEnd}
game={game}
moveCount={text ?? 0}
onTopOf={onTopOf}
theme={theme}
/>
);
Expand Down
1 change: 1 addition & 0 deletions constants/tileType.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ enum TileType {
NotDownOnExit = 'W',
LeftRightOnExit = 'X',
UpDownOnExit = 'Y',
PlayerOnExit = 'Z',
}

export default TileType;
Expand Down
13 changes: 9 additions & 4 deletions helpers/gameStateHelpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -175,10 +175,15 @@ export function initGameState(levelData: string) {
board[y][x].tileType = tileType;
} else if (TileTypeHelper.isOnExit(tileType)) {
board[y][x].tileType = TileType.Exit;
board[y][x].block = {
id: blockId++,
tileType: TileTypeHelper.getExitSibilingTileType(tileType),
} as BlockState;

if (tileType === TileType.PlayerOnExit) {
pos = new Position(x, y);
} else {
board[y][x].block = {
id: blockId++,
tileType: TileTypeHelper.getExitSibilingTileType(tileType),
} as BlockState;
}
} else if (tileType === TileType.Player) {
pos = new Position(x, y);
} else if (TileTypeHelper.canMove(tileType)) {
Expand Down
8 changes: 7 additions & 1 deletion helpers/tileTypeHelper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,9 @@ export default class TileTypeHelper {
exitSiblingTileType: TileType.BlockOnExit,
},
[TileType.Exit]: {},
[TileType.Player]: {},
[TileType.Player]: {
exitSiblingTileType: TileType.PlayerOnExit,
},
[TileType.Hole]: {},
[TileType.Left]: {
directions: [Direction.LEFT],
Expand Down Expand Up @@ -152,6 +154,10 @@ export default class TileTypeHelper {
exitSiblingTileType: TileType.UpDown,
isOnExit: true,
},
[TileType.PlayerOnExit]: {
exitSiblingTileType: TileType.Player,
isOnExit: true,
},
};

static canMove(tileType: TileType) {
Expand Down
18 changes: 13 additions & 5 deletions helpers/validators/validateSokopath.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,17 @@ import Position from '../../models/position';
export default function validateSokopathSolution(directions: Direction[], level: Level) {
const data = level.data.replace(/\n/g, '').split('') as TileType[];
const endIndices = [];
const posIndex = data.indexOf(TileType.Player);
const playerOnExitIndex = data.indexOf(TileType.PlayerOnExit);
const posIndex = playerOnExitIndex === -1 ? data.indexOf(TileType.Player) : playerOnExitIndex;

let pos = new Position(posIndex % level.width, Math.floor(posIndex / level.width));
let endIndex = -1;

while ((endIndex = data.indexOf(TileType.Exit, endIndex + 1)) != -1) {
endIndices.push(endIndex);
for (let i = 0; i < data.length; i++) {
const tileType = data[i];

if (tileType === TileType.Exit || TileTypeHelper.isOnExit(tileType)) {
endIndices.push(i);
}
}

for (let i = 0; i < directions.length; i++) {
Expand Down Expand Up @@ -89,7 +94,10 @@ export function validateSokopathLevel(data: string): {valid: boolean, reasons: s
for (let x = 0; x < width; x++) {
const tileType = dataSplit[y][x] as TileType;

if (tileType === TileType.Player) {
if (tileType === TileType.PlayerOnExit) {
startCount++;
goalCount++;
} else if (tileType === TileType.Player) {
startCount++;
} else {
if (tileType === TileType.Exit) {
Expand Down
4 changes: 2 additions & 2 deletions tests/constants/constants.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,9 @@ describe('constants/*.ts', () => {
expect(res).toBeUndefined();
});
test('getInvalidTileType with valid input', async () => {
const res = TileTypeHelper.getInvalidTileType('Z');
const res = TileTypeHelper.getInvalidTileType('?');

expect(res).toBe('Z');
expect(res).toBe('?');
});
});

Expand Down
11 changes: 11 additions & 0 deletions tests/helpers/helpers.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,17 @@ describe('helpers/*.ts', () => {
const gridWithOneStartAndOneEnd = '00' + TileType.Player + TileType.Exit;
const gridWithOneStartAndOneEndWithBlockOnTop = '00' + TileType.Player + TileType.BlockOnExit;

expect(validateSokopathLevel(emptyGrid).reasons).toMatchObject(['Must have exactly one player', 'Must have at least one uncovered goal']);
expect(validateSokopathLevel(gridWithOnlyOneStart).reasons).toMatchObject(['Must have at least one uncovered goal']);
expect(validateSokopathLevel(gridWithOneStartAndOneEnd).reasons).toMatchObject(['Must have as many boxes as goals']);
expect(validateSokopathLevel(gridWithOneStartAndOneEndWithBlockOnTop).valid).toBe(false);
});
test('validiateSokopathLevelValid', async () => {
const emptyGrid = '000';
const gridWithOnlyOneStart = '00' + TileType.Player;
const gridWithOneStartAndOneEnd = '00' + TileType.Player + TileType.Exit;
const gridWithOneStartAndOneEndWithBlockOnTop = '00' + TileType.PlayerOnExit + TileType.BlockOnExit;

expect(validateSokopathLevel(emptyGrid).reasons).toMatchObject(['Must have exactly one player', 'Must have at least one uncovered goal']);
expect(validateSokopathLevel(gridWithOnlyOneStart).reasons).toMatchObject(['Must have at least one uncovered goal']);
expect(validateSokopathLevel(gridWithOneStartAndOneEnd).reasons).toMatchObject(['Must have as many boxes as goals']);
Expand Down
Loading

0 comments on commit 0cada84

Please sign in to comment.