Compare commits
5 Commits
03d367c7b0
...
113d240f71
| Author | SHA1 | Date |
|---|---|---|
|
|
113d240f71 | |
|
|
0547180074 | |
|
|
d1605a9ee3 | |
|
|
093738cd42 | |
|
|
97ff61985a |
|
|
@ -3,5 +3,6 @@ export * from "./system/deck";
|
||||||
export * from "./system/encounter";
|
export * from "./system/encounter";
|
||||||
export * from "./system/grid-inventory";
|
export * from "./system/grid-inventory";
|
||||||
export * from "./system/map";
|
export * from "./system/map";
|
||||||
export * from "./system/utils/parse-shape";
|
export * from "./system/utils";
|
||||||
export * from "./system/types";
|
export * from "./system/types";
|
||||||
|
export { default as data } from "./data";
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,2 @@
|
||||||
export type { GameCard, GameCardMeta, PlayerDeck, DeckRegions } from './types';
|
export * from "./factory";
|
||||||
export {
|
export * from "./types";
|
||||||
generateDeckFromInventory,
|
|
||||||
createCard,
|
|
||||||
createPlayerDeck,
|
|
||||||
generateCardId,
|
|
||||||
} from './factory';
|
|
||||||
|
|
|
||||||
|
|
@ -1,24 +1,3 @@
|
||||||
export type {
|
export * from "./types";
|
||||||
CellCoordinate,
|
export * from "./transform";
|
||||||
CellKey,
|
|
||||||
GridInventory,
|
|
||||||
InventoryItem,
|
|
||||||
MutationResult,
|
|
||||||
PlacementResult,
|
|
||||||
} from "./types";
|
|
||||||
export type { GameItemMeta, GameItem } from "./types";
|
|
||||||
|
|
||||||
export {
|
|
||||||
createGridInventory,
|
|
||||||
flipItem,
|
|
||||||
getAdjacentItems,
|
|
||||||
getItemAtCell,
|
|
||||||
getOccupiedCellSet,
|
|
||||||
moveItem,
|
|
||||||
placeItem,
|
|
||||||
removeItem,
|
|
||||||
rotateItem,
|
|
||||||
validatePlacement,
|
|
||||||
} from "./transform";
|
|
||||||
|
|
||||||
export * from "./factory";
|
export * from "./factory";
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { EffectData } from "../types";
|
||||||
import type { ParsedShape } from "../utils/parse-shape";
|
import type { ParsedShape } from "../utils/parse-shape";
|
||||||
import type { Transform2D } from "../utils/shape-collision";
|
import type { Transform2D } from "../utils/shape-collision";
|
||||||
import {
|
import {
|
||||||
|
|
@ -10,6 +11,7 @@ import {
|
||||||
} from "../utils/shape-collision";
|
} from "../utils/shape-collision";
|
||||||
import type {
|
import type {
|
||||||
CellKey,
|
CellKey,
|
||||||
|
GameItemMeta,
|
||||||
GridInventory,
|
GridInventory,
|
||||||
InventoryItem,
|
InventoryItem,
|
||||||
MutationResult,
|
MutationResult,
|
||||||
|
|
@ -255,3 +257,21 @@ export function getAdjacentItems<TMeta>(
|
||||||
|
|
||||||
return adjacent;
|
return adjacent;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// export type EffectTable = Record<string, { data: EffectData; stacks: number }>;
|
||||||
|
export function getItemEffects(inv: GridInventory<GameItemMeta>) {
|
||||||
|
const effects = {} as Record<
|
||||||
|
string,
|
||||||
|
Record<string, { data: EffectData; stacks: number }>
|
||||||
|
>;
|
||||||
|
|
||||||
|
for (const item of inv.items.values()) {
|
||||||
|
if (!item.meta) continue;
|
||||||
|
const { startEffects } = item.meta;
|
||||||
|
if (!startEffects) continue;
|
||||||
|
|
||||||
|
effects[item.id] = startEffects;
|
||||||
|
}
|
||||||
|
|
||||||
|
return effects;
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { ItemData } from "../types";
|
import { EffectData, ItemData } from "../types";
|
||||||
import type { ParsedShape } from "../utils/parse-shape";
|
import type { ParsedShape } from "../utils/parse-shape";
|
||||||
import type { Transform2D } from "../utils/shape-collision";
|
import type { Transform2D } from "../utils/shape-collision";
|
||||||
|
|
||||||
|
|
@ -64,7 +64,7 @@ export interface GameItemMeta {
|
||||||
itemData: ItemData;
|
itemData: ItemData;
|
||||||
shape: ParsedShape;
|
shape: ParsedShape;
|
||||||
consumedUses?: number;
|
consumedUses?: number;
|
||||||
startEffects?: Record<string, number>;
|
startEffects?: Record<string, { data: EffectData; stacks: number }>;
|
||||||
tradePrice?: number;
|
tradePrice?: number;
|
||||||
}
|
}
|
||||||
export type GameItem = InventoryItem<GameItemMeta>;
|
export type GameItem = InventoryItem<GameItemMeta>;
|
||||||
|
|
|
||||||
|
|
@ -1,24 +1,3 @@
|
||||||
export { MapNodeType, MapLayerType } from "./types";
|
export * from "./generator";
|
||||||
export type {
|
export * from "./navigation";
|
||||||
MapNode,
|
export * from "./types";
|
||||||
MapLayer,
|
|
||||||
PointCrawlMap,
|
|
||||||
MapGenerationConfig,
|
|
||||||
} from "./types";
|
|
||||||
|
|
||||||
export { generatePointCrawlMap } from "./generator";
|
|
||||||
export {
|
|
||||||
getNode,
|
|
||||||
getChildren,
|
|
||||||
getParents,
|
|
||||||
hasPath,
|
|
||||||
findAllPaths,
|
|
||||||
} from "./generator";
|
|
||||||
|
|
||||||
export {
|
|
||||||
canMoveTo,
|
|
||||||
moveToNode,
|
|
||||||
getReachableChildren,
|
|
||||||
isAtEndNode,
|
|
||||||
isAtStartNode,
|
|
||||||
} from "./navigation";
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
export * from "./parse-shape";
|
||||||
|
export * from "./shape-collision";
|
||||||
|
|
@ -1,129 +1,132 @@
|
||||||
import type { ParsedShape } from './parse-shape';
|
import type { ParsedShape } from "./parse-shape";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Represents a 2D point in grid coordinates.
|
* Represents a 2D point in grid coordinates.
|
||||||
*/
|
*/
|
||||||
export interface Point2D {
|
export interface Point2D {
|
||||||
x: number;
|
x: number;
|
||||||
y: number;
|
y: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 2D transformation to apply to a shape.
|
* 2D transformation to apply to a shape.
|
||||||
*/
|
*/
|
||||||
export interface Transform2D {
|
export interface Transform2D {
|
||||||
/** Translation offset in grid units */
|
/** Translation offset in grid units */
|
||||||
offset: Point2D;
|
offset: Point2D;
|
||||||
/** Rotation in degrees (0, 90, 180, 270) */
|
/** Rotation in degrees (0, 90, 180, 270) */
|
||||||
rotation: number;
|
rotation: number;
|
||||||
/** Whether to flip horizontally */
|
/** Whether to flip horizontally */
|
||||||
flipX: boolean;
|
flipX: boolean;
|
||||||
/** Whether to flip vertically */
|
/** Whether to flip vertically */
|
||||||
flipY: boolean;
|
flipY: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Default transform (identity).
|
* Default transform (identity).
|
||||||
*/
|
*/
|
||||||
export const IDENTITY_TRANSFORM: Transform2D = {
|
export const IDENTITY_TRANSFORM: Transform2D = {
|
||||||
offset: { x: 0, y: 0 },
|
offset: { x: 0, y: 0 },
|
||||||
rotation: 0,
|
rotation: 0,
|
||||||
flipX: false,
|
flipX: false,
|
||||||
flipY: false,
|
flipY: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets all occupied cell coordinates from a shape.
|
* Gets all occupied cell coordinates from a shape.
|
||||||
*/
|
*/
|
||||||
export function getOccupiedCells(shape: ParsedShape): Point2D[] {
|
export function getOccupiedCells(shape: ParsedShape): Point2D[] {
|
||||||
const cells: Point2D[] = [];
|
const cells: Point2D[] = [];
|
||||||
for (let y = 0; y < shape.height; y++) {
|
for (let y = 0; y < shape.height; y++) {
|
||||||
for (let x = 0; x < shape.width; x++) {
|
for (let x = 0; x < shape.width; x++) {
|
||||||
if (shape.grid[y]?.[x]) {
|
if (shape.grid[y]?.[x]) {
|
||||||
cells.push({ x, y });
|
cells.push({ x, y });
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return cells;
|
}
|
||||||
|
return cells;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Applies a 2D transformation to a point.
|
* Applies a 2D transformation to a point.
|
||||||
*/
|
*/
|
||||||
export function transformPoint(
|
export function transformPoint(
|
||||||
point: Point2D,
|
point: Point2D,
|
||||||
transform: Transform2D,
|
transform: Transform2D,
|
||||||
shapeWidth: number,
|
shapeWidth: number,
|
||||||
shapeHeight: number
|
shapeHeight: number,
|
||||||
): Point2D {
|
): Point2D {
|
||||||
let { x, y } = point;
|
let { x, y } = point;
|
||||||
|
|
||||||
// Apply flips
|
// Apply flips
|
||||||
if (transform.flipX) {
|
if (transform.flipX) {
|
||||||
x = shapeWidth - 1 - x;
|
x = shapeWidth - 1 - x;
|
||||||
}
|
}
|
||||||
if (transform.flipY) {
|
if (transform.flipY) {
|
||||||
y = shapeHeight - 1 - y;
|
y = shapeHeight - 1 - y;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Apply rotation (around origin 0,0)
|
// Apply rotation (around origin 0,0)
|
||||||
const rotation = ((transform.rotation % 360) + 360) % 360;
|
const rotation = ((transform.rotation % 360) + 360) % 360;
|
||||||
let rotatedX = x;
|
let rotatedX = x;
|
||||||
let rotatedY = y;
|
let rotatedY = y;
|
||||||
|
|
||||||
switch (rotation) {
|
switch (rotation) {
|
||||||
case 90:
|
case 90:
|
||||||
rotatedX = y;
|
rotatedX = y;
|
||||||
rotatedY = -x;
|
rotatedY = -x;
|
||||||
break;
|
break;
|
||||||
case 180:
|
case 180:
|
||||||
rotatedX = -x;
|
rotatedX = -x;
|
||||||
rotatedY = -y;
|
rotatedY = -y;
|
||||||
break;
|
break;
|
||||||
case 270:
|
case 270:
|
||||||
rotatedX = -y;
|
rotatedX = -y;
|
||||||
rotatedY = x;
|
rotatedY = x;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Apply offset
|
// Apply offset
|
||||||
return {
|
return {
|
||||||
x: rotatedX + transform.offset.x,
|
x: rotatedX + transform.offset.x,
|
||||||
y: rotatedY + transform.offset.y,
|
y: rotatedY + transform.offset.y,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Transforms a shape and returnss its occupied cells in world coordinates.
|
* Transforms a shape and returnss its occupied cells in world coordinates.
|
||||||
*/
|
*/
|
||||||
export function transformShape(shape: ParsedShape, transform: Transform2D): Point2D[] {
|
export function transformShape(
|
||||||
const cells = getOccupiedCells(shape);
|
shape: ParsedShape,
|
||||||
return cells.map(cell =>
|
transform: Transform2D,
|
||||||
transformPoint(cell, transform, shape.width, shape.height)
|
): Point2D[] {
|
||||||
);
|
const cells = getOccupiedCells(shape);
|
||||||
|
return cells.map((cell) =>
|
||||||
|
transformPoint(cell, transform, shape.width, shape.height),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Checks if two transformed shapes collide (share any occupied cell).
|
* Checks if two transformed shapes collide (share any occupied cell).
|
||||||
*/
|
*/
|
||||||
export function checkCollision(
|
export function checkCollision(
|
||||||
shapeA: ParsedShape,
|
shapeA: ParsedShape,
|
||||||
transformA: Transform2D,
|
transformA: Transform2D,
|
||||||
shapeB: ParsedShape,
|
shapeB: ParsedShape,
|
||||||
transformB: Transform2D
|
transformB: Transform2D,
|
||||||
): boolean {
|
): boolean {
|
||||||
const cellsA = transformShape(shapeA, transformA);
|
const cellsA = transformShape(shapeA, transformA);
|
||||||
const cellsB = transformShape(shapeB, transformB);
|
const cellsB = transformShape(shapeB, transformB);
|
||||||
|
|
||||||
const setA = new Set(cellsA.map(c => `${c.x},${c.y}`));
|
const setA = new Set(cellsA.map((c) => `${c.x},${c.y}`));
|
||||||
|
|
||||||
for (const cell of cellsB) {
|
for (const cell of cellsB) {
|
||||||
if (setA.has(`${cell.x},${cell.y}`)) {
|
if (setA.has(`${cell.x},${cell.y}`)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -133,19 +136,19 @@ export function checkCollision(
|
||||||
* @param occupiedCells Set of occupied board cells in "x,y" format
|
* @param occupiedCells Set of occupied board cells in "x,y" format
|
||||||
*/
|
*/
|
||||||
export function checkBoardCollision(
|
export function checkBoardCollision(
|
||||||
shape: ParsedShape,
|
shape: ParsedShape,
|
||||||
transform: Transform2D,
|
transform: Transform2D,
|
||||||
occupiedCells: Set<string>
|
occupiedCells: Set<string>,
|
||||||
): boolean {
|
): boolean {
|
||||||
const cells = transformShape(shape, transform);
|
const cells = transformShape(shape, transform);
|
||||||
|
|
||||||
for (const cell of cells) {
|
for (const cell of cells) {
|
||||||
if (occupiedCells.has(`${cell.x},${cell.y}`)) {
|
if (occupiedCells.has(`${cell.x},${cell.y}`)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -156,42 +159,47 @@ export function checkBoardCollision(
|
||||||
* @param boardHeight Board height
|
* @param boardHeight Board height
|
||||||
*/
|
*/
|
||||||
export function checkBounds(
|
export function checkBounds(
|
||||||
shape: ParsedShape,
|
shape: ParsedShape,
|
||||||
transform: Transform2D,
|
transform: Transform2D,
|
||||||
boardWidth: number,
|
boardWidth: number,
|
||||||
boardHeight: number
|
boardHeight: number,
|
||||||
): boolean {
|
): boolean {
|
||||||
const cells = transformShape(shape, transform);
|
const cells = transformShape(shape, transform);
|
||||||
|
|
||||||
for (const cell of cells) {
|
for (const cell of cells) {
|
||||||
if (cell.x < 0 || cell.x >= boardWidth || cell.y < 0 || cell.y >= boardHeight) {
|
if (
|
||||||
return false;
|
cell.x < 0 ||
|
||||||
}
|
cell.x >= boardWidth ||
|
||||||
|
cell.y < 0 ||
|
||||||
|
cell.y >= boardHeight
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Validates that a placement is both in bounds and collision-free.
|
* Validates that a placement is both in bounds and collision-free.
|
||||||
* @returns Object with `valid` flag and optional `reason` string
|
* @returns Object with `valid` flag and optional `reason` string
|
||||||
*/
|
*/
|
||||||
export function validatePlacement(
|
export function validateShapePlacement(
|
||||||
shape: ParsedShape,
|
shape: ParsedShape,
|
||||||
transform: Transform2D,
|
transform: Transform2D,
|
||||||
boardWidth: number,
|
boardWidth: number,
|
||||||
boardHeight: number,
|
boardHeight: number,
|
||||||
occupiedCells: Set<string>
|
occupiedCells: Set<string>,
|
||||||
): { valid: true } | { valid: false; reason: string } {
|
): { valid: true } | { valid: false; reason: string } {
|
||||||
if (!checkBounds(shape, transform, boardWidth, boardHeight)) {
|
if (!checkBounds(shape, transform, boardWidth, boardHeight)) {
|
||||||
return { valid: false, reason: '超出边界' };
|
return { valid: false, reason: "超出边界" };
|
||||||
}
|
}
|
||||||
|
|
||||||
if (checkBoardCollision(shape, transform, occupiedCells)) {
|
if (checkBoardCollision(shape, transform, occupiedCells)) {
|
||||||
return { valid: false, reason: '与已有形状重叠' };
|
return { valid: false, reason: "与已有形状重叠" };
|
||||||
}
|
}
|
||||||
|
|
||||||
return { valid: true };
|
return { valid: true };
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -199,29 +207,32 @@ export function validatePlacement(
|
||||||
* @param current The current transform
|
* @param current The current transform
|
||||||
* @param degrees Degrees to rotate (typically 90, 180, or 270)
|
* @param degrees Degrees to rotate (typically 90, 180, or 270)
|
||||||
*/
|
*/
|
||||||
export function rotateTransform(current: Transform2D, degrees: number): Transform2D {
|
export function rotateTransform(
|
||||||
return {
|
current: Transform2D,
|
||||||
...current,
|
degrees: number,
|
||||||
rotation: ((current.rotation + degrees) % 360 + 360) % 360,
|
): Transform2D {
|
||||||
};
|
return {
|
||||||
|
...current,
|
||||||
|
rotation: (((current.rotation + degrees) % 360) + 360) % 360,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Flips a transform horizontally.
|
* Flips a transform horizontally.
|
||||||
*/
|
*/
|
||||||
export function flipXTransform(current: Transform2D): Transform2D {
|
export function flipXTransform(current: Transform2D): Transform2D {
|
||||||
return {
|
return {
|
||||||
...current,
|
...current,
|
||||||
flipX: !current.flipX,
|
flipX: !current.flipX,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Flips a transform vertically.
|
* Flips a transform vertically.
|
||||||
*/
|
*/
|
||||||
export function flipYTransform(current: Transform2D): Transform2D {
|
export function flipYTransform(current: Transform2D): Transform2D {
|
||||||
return {
|
return {
|
||||||
...current,
|
...current,
|
||||||
flipY: !current.flipY,
|
flipY: !current.flipY,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,355 +1,367 @@
|
||||||
import { describe, it, expect } from 'vitest';
|
import { describe, it, expect } from "vitest";
|
||||||
import { parseShapeString } from '@/samples/slay-the-spire-like/system/utils/parse-shape';
|
import { parseShapeString } from "@/samples/slay-the-spire-like/system/utils/parse-shape";
|
||||||
import {
|
import {
|
||||||
checkCollision,
|
checkCollision,
|
||||||
checkBoardCollision,
|
checkBoardCollision,
|
||||||
checkBounds,
|
checkBounds,
|
||||||
validatePlacement,
|
validateShapePlacement,
|
||||||
transformShape,
|
transformShape,
|
||||||
getOccupiedCells,
|
getOccupiedCells,
|
||||||
IDENTITY_TRANSFORM,
|
IDENTITY_TRANSFORM,
|
||||||
} from '@/samples/slay-the-spire-like/system/utils/shape-collision';
|
} from "@/samples/slay-the-spire-like/system/utils/shape-collision";
|
||||||
|
|
||||||
describe('parseShapeString', () => {
|
describe("parseShapeString", () => {
|
||||||
it('should parse a single cell with o', () => {
|
it("should parse a single cell with o", () => {
|
||||||
const result = parseShapeString('o');
|
const result = parseShapeString("o");
|
||||||
expect(result.grid).toEqual([[true]]);
|
expect(result.grid).toEqual([[true]]);
|
||||||
expect(result.width).toBe(1);
|
expect(result.width).toBe(1);
|
||||||
expect(result.height).toBe(1);
|
expect(result.height).toBe(1);
|
||||||
expect(result.count).toBe(1);
|
expect(result.count).toBe(1);
|
||||||
expect(result.originX).toBe(0);
|
expect(result.originX).toBe(0);
|
||||||
expect(result.originY).toBe(0);
|
expect(result.originY).toBe(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should parse a horizontal line', () => {
|
it("should parse a horizontal line", () => {
|
||||||
const result = parseShapeString('oee');
|
const result = parseShapeString("oee");
|
||||||
expect(result.width).toBe(3);
|
expect(result.width).toBe(3);
|
||||||
expect(result.height).toBe(1);
|
expect(result.height).toBe(1);
|
||||||
expect(result.count).toBe(3);
|
expect(result.count).toBe(3);
|
||||||
expect(result.grid).toEqual([[true, true, true]]);
|
expect(result.grid).toEqual([[true, true, true]]);
|
||||||
expect(result.originX).toBe(0);
|
expect(result.originX).toBe(0);
|
||||||
expect(result.originY).toBe(0);
|
expect(result.originY).toBe(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should parse a vertical line', () => {
|
it("should parse a vertical line", () => {
|
||||||
const result = parseShapeString('oss');
|
const result = parseShapeString("oss");
|
||||||
expect(result.width).toBe(1);
|
expect(result.width).toBe(1);
|
||||||
expect(result.height).toBe(3);
|
expect(result.height).toBe(3);
|
||||||
expect(result.count).toBe(3);
|
expect(result.count).toBe(3);
|
||||||
expect(result.grid).toEqual([[true], [true], [true]]);
|
expect(result.grid).toEqual([[true], [true], [true]]);
|
||||||
expect(result.originX).toBe(0);
|
expect(result.originX).toBe(0);
|
||||||
expect(result.originY).toBe(0);
|
expect(result.originY).toBe(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should parse an L shape', () => {
|
it("should parse an L shape", () => {
|
||||||
const result = parseShapeString('oes');
|
const result = parseShapeString("oes");
|
||||||
expect(result.width).toBe(2);
|
expect(result.width).toBe(2);
|
||||||
expect(result.height).toBe(2);
|
expect(result.height).toBe(2);
|
||||||
expect(result.count).toBe(3);
|
expect(result.count).toBe(3);
|
||||||
expect(result.grid).toEqual([
|
expect(result.grid).toEqual([
|
||||||
[true, true],
|
[true, true],
|
||||||
[false, true],
|
[false, true],
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should handle return command', () => {
|
it("should handle return command", () => {
|
||||||
const result = parseShapeString('oeerww');
|
const result = parseShapeString("oeerww");
|
||||||
expect(result.width).toBe(4);
|
expect(result.width).toBe(4);
|
||||||
expect(result.height).toBe(1);
|
expect(result.height).toBe(1);
|
||||||
expect(result.count).toBe(4);
|
expect(result.count).toBe(4);
|
||||||
expect(result.grid).toEqual([[true, true, true, true]]);
|
expect(result.grid).toEqual([[true, true, true, true]]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should handle case insensitivity', () => {
|
it("should handle case insensitivity", () => {
|
||||||
const resultLower = parseShapeString('oes');
|
const resultLower = parseShapeString("oes");
|
||||||
const resultUpper = parseShapeString('OES');
|
const resultUpper = parseShapeString("OES");
|
||||||
expect(resultLower.grid).toEqual(resultUpper.grid);
|
expect(resultLower.grid).toEqual(resultUpper.grid);
|
||||||
expect(resultLower.count).toBe(resultUpper.count);
|
expect(resultLower.count).toBe(resultUpper.count);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return empty grid for empty input', () => {
|
it("should return empty grid for empty input", () => {
|
||||||
const result = parseShapeString('');
|
const result = parseShapeString("");
|
||||||
expect(result.grid).toEqual([[]]);
|
expect(result.grid).toEqual([[]]);
|
||||||
expect(result.width).toBe(0);
|
expect(result.width).toBe(0);
|
||||||
expect(result.height).toBe(1);
|
expect(result.height).toBe(1);
|
||||||
expect(result.count).toBe(0);
|
expect(result.count).toBe(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should track origin correctly', () => {
|
it("should track origin correctly", () => {
|
||||||
// eeso: e(1,0), e(2,0), s(2,1), o sets origin at (2,1)
|
// eeso: e(1,0), e(2,0), s(2,1), o sets origin at (2,1)
|
||||||
// After normalization: minX=1, minY=0, so originX = 2-1 = 1, originY = 1-0 = 1
|
// After normalization: minX=1, minY=0, so originX = 2-1 = 1, originY = 1-0 = 1
|
||||||
const result = parseShapeString('eeso');
|
const result = parseShapeString("eeso");
|
||||||
expect(result.originX).toBe(1);
|
expect(result.originX).toBe(1);
|
||||||
expect(result.originY).toBe(1);
|
expect(result.originY).toBe(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should track origin at first o only', () => {
|
it("should track origin at first o only", () => {
|
||||||
const result = parseShapeString('oes');
|
const result = parseShapeString("oes");
|
||||||
expect(result.originX).toBe(0);
|
expect(result.originX).toBe(0);
|
||||||
expect(result.originY).toBe(0);
|
expect(result.originY).toBe(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should handle complex T shape', () => {
|
it("should handle complex T shape", () => {
|
||||||
// oewers: o(0,0), e(1,0), w(0,0), e(1,0), r->(0,0), s(0,1)
|
// oewers: o(0,0), e(1,0), w(0,0), e(1,0), r->(0,0), s(0,1)
|
||||||
// Filled: (0,0), (1,0), (0,1) - 3 cells
|
// Filled: (0,0), (1,0), (0,1) - 3 cells
|
||||||
const result = parseShapeString('oewers');
|
const result = parseShapeString("oewers");
|
||||||
expect(result.width).toBe(2);
|
expect(result.width).toBe(2);
|
||||||
expect(result.height).toBe(2);
|
expect(result.height).toBe(2);
|
||||||
expect(result.count).toBe(3);
|
expect(result.count).toBe(3);
|
||||||
expect(result.grid).toEqual([
|
expect(result.grid).toEqual([
|
||||||
[true, true],
|
[true, true],
|
||||||
[true, false],
|
[true, false],
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('shape-collision', () => {
|
describe("shape-collision", () => {
|
||||||
describe('getOccupiedCells', () => {
|
describe("getOccupiedCells", () => {
|
||||||
it('should return cells for a single cell shape', () => {
|
it("should return cells for a single cell shape", () => {
|
||||||
const shape = parseShapeString('o');
|
const shape = parseShapeString("o");
|
||||||
const cells = getOccupiedCells(shape);
|
const cells = getOccupiedCells(shape);
|
||||||
expect(cells).toEqual([{ x: 0, y: 0 }]);
|
expect(cells).toEqual([{ x: 0, y: 0 }]);
|
||||||
});
|
|
||||||
|
|
||||||
it('should return cells for a horizontal line', () => {
|
|
||||||
const shape = parseShapeString('oe');
|
|
||||||
const cells = getOccupiedCells(shape);
|
|
||||||
expect(cells).toEqual([
|
|
||||||
{ x: 0, y: 0 },
|
|
||||||
{ x: 1, y: 0 },
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return cells for an L shape', () => {
|
|
||||||
const shape = parseShapeString('oes');
|
|
||||||
const cells = getOccupiedCells(shape);
|
|
||||||
expect(cells).toEqual([
|
|
||||||
{ x: 0, y: 0 },
|
|
||||||
{ x: 1, y: 0 },
|
|
||||||
{ x: 1, y: 1 },
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('checkCollision', () => {
|
it("should return cells for a horizontal line", () => {
|
||||||
it('should detect collision between overlapping shapes', () => {
|
const shape = parseShapeString("oe");
|
||||||
const shapeA = parseShapeString('o');
|
const cells = getOccupiedCells(shape);
|
||||||
const shapeB = parseShapeString('o');
|
expect(cells).toEqual([
|
||||||
|
{ x: 0, y: 0 },
|
||||||
const result = checkCollision(
|
{ x: 1, y: 0 },
|
||||||
shapeA,
|
]);
|
||||||
{ ...IDENTITY_TRANSFORM, offset: { x: 0, y: 0 } },
|
|
||||||
shapeB,
|
|
||||||
{ ...IDENTITY_TRANSFORM, offset: { x: 0, y: 0 } }
|
|
||||||
);
|
|
||||||
expect(result).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should not detect collision between non-overlapping shapes', () => {
|
|
||||||
const shapeA = parseShapeString('o');
|
|
||||||
const shapeB = parseShapeString('o');
|
|
||||||
|
|
||||||
const result = checkCollision(
|
|
||||||
shapeA,
|
|
||||||
{ ...IDENTITY_TRANSFORM, offset: { x: 0, y: 0 } },
|
|
||||||
shapeB,
|
|
||||||
{ ...IDENTITY_TRANSFORM, offset: { x: 2, y: 0 } }
|
|
||||||
);
|
|
||||||
expect(result).toBe(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should detect collision with adjacent shapes', () => {
|
|
||||||
const shapeA = parseShapeString('o');
|
|
||||||
const shapeB = parseShapeString('o');
|
|
||||||
|
|
||||||
const result = checkCollision(
|
|
||||||
shapeA,
|
|
||||||
{ ...IDENTITY_TRANSFORM, offset: { x: 0, y: 0 } },
|
|
||||||
shapeB,
|
|
||||||
{ ...IDENTITY_TRANSFORM, offset: { x: 1, y: 0 } }
|
|
||||||
);
|
|
||||||
expect(result).toBe(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should detect collision with rotation', () => {
|
|
||||||
const shapeA = parseShapeString('oe');
|
|
||||||
const shapeB = parseShapeString('os');
|
|
||||||
|
|
||||||
// shapeA is horizontal at (0,0)-(1,0)
|
|
||||||
// shapeB rotated 90° becomes vertical at (0,0)-(0,1)
|
|
||||||
// They should collide at (0,0)
|
|
||||||
const result = checkCollision(
|
|
||||||
shapeA,
|
|
||||||
{ ...IDENTITY_TRANSFORM, offset: { x: 0, y: 0 } },
|
|
||||||
shapeB,
|
|
||||||
{ ...IDENTITY_TRANSFORM, rotation: 90, offset: { x: 0, y: 0 } }
|
|
||||||
);
|
|
||||||
expect(result).toBe(true);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('checkBoardCollision', () => {
|
it("should return cells for an L shape", () => {
|
||||||
it('should detect collision with occupied cells', () => {
|
const shape = parseShapeString("oes");
|
||||||
const shape = parseShapeString('oe');
|
const cells = getOccupiedCells(shape);
|
||||||
const occupied = new Set(['0,0', '1,0']);
|
expect(cells).toEqual([
|
||||||
|
{ x: 0, y: 0 },
|
||||||
|
{ x: 1, y: 0 },
|
||||||
|
{ x: 1, y: 1 },
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
const result = checkBoardCollision(shape, IDENTITY_TRANSFORM, occupied);
|
describe("checkCollision", () => {
|
||||||
expect(result).toBe(true);
|
it("should detect collision between overlapping shapes", () => {
|
||||||
});
|
const shapeA = parseShapeString("o");
|
||||||
|
const shapeB = parseShapeString("o");
|
||||||
|
|
||||||
it('should not detect collision with empty board', () => {
|
const result = checkCollision(
|
||||||
const shape = parseShapeString('oe');
|
shapeA,
|
||||||
const occupied = new Set<string>();
|
{ ...IDENTITY_TRANSFORM, offset: { x: 0, y: 0 } },
|
||||||
|
shapeB,
|
||||||
const result = checkBoardCollision(shape, IDENTITY_TRANSFORM, occupied);
|
{ ...IDENTITY_TRANSFORM, offset: { x: 0, y: 0 } },
|
||||||
expect(result).toBe(false);
|
);
|
||||||
});
|
expect(result).toBe(true);
|
||||||
|
|
||||||
it('should detect collision after translation', () => {
|
|
||||||
const shape = parseShapeString('oe');
|
|
||||||
const occupied = new Set(['5,5', '6,5']);
|
|
||||||
|
|
||||||
const result = checkBoardCollision(
|
|
||||||
shape,
|
|
||||||
{ ...IDENTITY_TRANSFORM, offset: { x: 5, y: 5 } },
|
|
||||||
occupied
|
|
||||||
);
|
|
||||||
expect(result).toBe(true);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('checkBounds', () => {
|
it("should not detect collision between non-overlapping shapes", () => {
|
||||||
it('should return true for shape within bounds', () => {
|
const shapeA = parseShapeString("o");
|
||||||
const shape = parseShapeString('oe');
|
const shapeB = parseShapeString("o");
|
||||||
|
|
||||||
const result = checkBounds(shape, IDENTITY_TRANSFORM, 10, 10);
|
const result = checkCollision(
|
||||||
expect(result).toBe(true);
|
shapeA,
|
||||||
});
|
{ ...IDENTITY_TRANSFORM, offset: { x: 0, y: 0 } },
|
||||||
|
shapeB,
|
||||||
it('should return false for shape outside bounds', () => {
|
{ ...IDENTITY_TRANSFORM, offset: { x: 2, y: 0 } },
|
||||||
const shape = parseShapeString('oe');
|
);
|
||||||
|
expect(result).toBe(false);
|
||||||
const result = checkBounds(
|
|
||||||
shape,
|
|
||||||
{ ...IDENTITY_TRANSFORM, offset: { x: 9, y: 0 } },
|
|
||||||
10,
|
|
||||||
10
|
|
||||||
);
|
|
||||||
expect(result).toBe(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return false for negative coordinates', () => {
|
|
||||||
const shape = parseShapeString('oe');
|
|
||||||
|
|
||||||
const result = checkBounds(
|
|
||||||
shape,
|
|
||||||
{ ...IDENTITY_TRANSFORM, offset: { x: -1, y: 0 } },
|
|
||||||
10,
|
|
||||||
10
|
|
||||||
);
|
|
||||||
expect(result).toBe(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return true for shape at boundary edge', () => {
|
|
||||||
const shape = parseShapeString('o');
|
|
||||||
|
|
||||||
const result = checkBounds(
|
|
||||||
shape,
|
|
||||||
{ ...IDENTITY_TRANSFORM, offset: { x: 9, y: 9 } },
|
|
||||||
10,
|
|
||||||
10
|
|
||||||
);
|
|
||||||
expect(result).toBe(true);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('validatePlacement', () => {
|
it("should detect collision with adjacent shapes", () => {
|
||||||
it('should return valid for good placement', () => {
|
const shapeA = parseShapeString("o");
|
||||||
const shape = parseShapeString('oe');
|
const shapeB = parseShapeString("o");
|
||||||
const occupied = new Set<string>();
|
|
||||||
|
|
||||||
const result = validatePlacement(shape, IDENTITY_TRANSFORM, 10, 10, occupied);
|
const result = checkCollision(
|
||||||
expect(result).toEqual({ valid: true });
|
shapeA,
|
||||||
});
|
{ ...IDENTITY_TRANSFORM, offset: { x: 0, y: 0 } },
|
||||||
|
shapeB,
|
||||||
it('should return invalid for out of bounds', () => {
|
{ ...IDENTITY_TRANSFORM, offset: { x: 1, y: 0 } },
|
||||||
const shape = parseShapeString('oe');
|
);
|
||||||
const occupied = new Set<string>();
|
expect(result).toBe(false);
|
||||||
|
|
||||||
const result = validatePlacement(
|
|
||||||
shape,
|
|
||||||
{ ...IDENTITY_TRANSFORM, offset: { x: 9, y: 0 } },
|
|
||||||
10,
|
|
||||||
10,
|
|
||||||
occupied
|
|
||||||
);
|
|
||||||
expect(result).toEqual({ valid: false, reason: '超出边界' });
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return invalid for collision', () => {
|
|
||||||
const shape = parseShapeString('oe');
|
|
||||||
const occupied = new Set(['0,0', '1,0']);
|
|
||||||
|
|
||||||
const result = validatePlacement(shape, IDENTITY_TRANSFORM, 10, 10, occupied);
|
|
||||||
expect(result).toEqual({ valid: false, reason: '与已有形状重叠' });
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('transformShape', () => {
|
it("should detect collision with rotation", () => {
|
||||||
it('should apply translation correctly', () => {
|
const shapeA = parseShapeString("oe");
|
||||||
const shape = parseShapeString('o');
|
const shapeB = parseShapeString("os");
|
||||||
const transform = { ...IDENTITY_TRANSFORM, offset: { x: 5, y: 3 } };
|
|
||||||
|
|
||||||
const cells = transformShape(shape, transform);
|
// shapeA is horizontal at (0,0)-(1,0)
|
||||||
expect(cells).toEqual([{ x: 5, y: 3 }]);
|
// shapeB rotated 90° becomes vertical at (0,0)-(0,1)
|
||||||
});
|
// They should collide at (0,0)
|
||||||
|
const result = checkCollision(
|
||||||
it('should apply 90° rotation correctly', () => {
|
shapeA,
|
||||||
const shape = parseShapeString('oe');
|
{ ...IDENTITY_TRANSFORM, offset: { x: 0, y: 0 } },
|
||||||
const transform = { ...IDENTITY_TRANSFORM, rotation: 90 };
|
shapeB,
|
||||||
|
{ ...IDENTITY_TRANSFORM, rotation: 90, offset: { x: 0, y: 0 } },
|
||||||
const cells = transformShape(shape, transform);
|
);
|
||||||
expect(cells).toEqual([
|
expect(result).toBe(true);
|
||||||
{ x: 0, y: 0 },
|
|
||||||
{ x: 0, y: -1 },
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should apply horizontal flip correctly', () => {
|
|
||||||
const shape = parseShapeString('oe');
|
|
||||||
const transform = { ...IDENTITY_TRANSFORM, flipX: true };
|
|
||||||
|
|
||||||
const cells = transformShape(shape, transform);
|
|
||||||
expect(cells).toEqual([
|
|
||||||
{ x: 1, y: 0 },
|
|
||||||
{ x: 0, y: 0 },
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should apply vertical flip correctly', () => {
|
|
||||||
const shape = parseShapeString('os');
|
|
||||||
const transform = { ...IDENTITY_TRANSFORM, flipY: true };
|
|
||||||
|
|
||||||
const cells = transformShape(shape, transform);
|
|
||||||
expect(cells).toEqual([
|
|
||||||
{ x: 0, y: 1 },
|
|
||||||
{ x: 0, y: 0 },
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should combine rotation and translation', () => {
|
|
||||||
const shape = parseShapeString('os');
|
|
||||||
const transform = {
|
|
||||||
...IDENTITY_TRANSFORM,
|
|
||||||
rotation: 90,
|
|
||||||
offset: { x: 10, y: 10 },
|
|
||||||
};
|
|
||||||
|
|
||||||
const cells = transformShape(shape, transform);
|
|
||||||
expect(cells).toEqual([
|
|
||||||
{ x: 10, y: 10 },
|
|
||||||
{ x: 11, y: 10 },
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("checkBoardCollision", () => {
|
||||||
|
it("should detect collision with occupied cells", () => {
|
||||||
|
const shape = parseShapeString("oe");
|
||||||
|
const occupied = new Set(["0,0", "1,0"]);
|
||||||
|
|
||||||
|
const result = checkBoardCollision(shape, IDENTITY_TRANSFORM, occupied);
|
||||||
|
expect(result).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should not detect collision with empty board", () => {
|
||||||
|
const shape = parseShapeString("oe");
|
||||||
|
const occupied = new Set<string>();
|
||||||
|
|
||||||
|
const result = checkBoardCollision(shape, IDENTITY_TRANSFORM, occupied);
|
||||||
|
expect(result).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should detect collision after translation", () => {
|
||||||
|
const shape = parseShapeString("oe");
|
||||||
|
const occupied = new Set(["5,5", "6,5"]);
|
||||||
|
|
||||||
|
const result = checkBoardCollision(
|
||||||
|
shape,
|
||||||
|
{ ...IDENTITY_TRANSFORM, offset: { x: 5, y: 5 } },
|
||||||
|
occupied,
|
||||||
|
);
|
||||||
|
expect(result).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("checkBounds", () => {
|
||||||
|
it("should return true for shape within bounds", () => {
|
||||||
|
const shape = parseShapeString("oe");
|
||||||
|
|
||||||
|
const result = checkBounds(shape, IDENTITY_TRANSFORM, 10, 10);
|
||||||
|
expect(result).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return false for shape outside bounds", () => {
|
||||||
|
const shape = parseShapeString("oe");
|
||||||
|
|
||||||
|
const result = checkBounds(
|
||||||
|
shape,
|
||||||
|
{ ...IDENTITY_TRANSFORM, offset: { x: 9, y: 0 } },
|
||||||
|
10,
|
||||||
|
10,
|
||||||
|
);
|
||||||
|
expect(result).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return false for negative coordinates", () => {
|
||||||
|
const shape = parseShapeString("oe");
|
||||||
|
|
||||||
|
const result = checkBounds(
|
||||||
|
shape,
|
||||||
|
{ ...IDENTITY_TRANSFORM, offset: { x: -1, y: 0 } },
|
||||||
|
10,
|
||||||
|
10,
|
||||||
|
);
|
||||||
|
expect(result).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return true for shape at boundary edge", () => {
|
||||||
|
const shape = parseShapeString("o");
|
||||||
|
|
||||||
|
const result = checkBounds(
|
||||||
|
shape,
|
||||||
|
{ ...IDENTITY_TRANSFORM, offset: { x: 9, y: 9 } },
|
||||||
|
10,
|
||||||
|
10,
|
||||||
|
);
|
||||||
|
expect(result).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("validatePlacement", () => {
|
||||||
|
it("should return valid for good placement", () => {
|
||||||
|
const shape = parseShapeString("oe");
|
||||||
|
const occupied = new Set<string>();
|
||||||
|
|
||||||
|
const result = validateShapePlacement(
|
||||||
|
shape,
|
||||||
|
IDENTITY_TRANSFORM,
|
||||||
|
10,
|
||||||
|
10,
|
||||||
|
occupied,
|
||||||
|
);
|
||||||
|
expect(result).toEqual({ valid: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return invalid for out of bounds", () => {
|
||||||
|
const shape = parseShapeString("oe");
|
||||||
|
const occupied = new Set<string>();
|
||||||
|
|
||||||
|
const result = validateShapePlacement(
|
||||||
|
shape,
|
||||||
|
{ ...IDENTITY_TRANSFORM, offset: { x: 9, y: 0 } },
|
||||||
|
10,
|
||||||
|
10,
|
||||||
|
occupied,
|
||||||
|
);
|
||||||
|
expect(result).toEqual({ valid: false, reason: "超出边界" });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return invalid for collision", () => {
|
||||||
|
const shape = parseShapeString("oe");
|
||||||
|
const occupied = new Set(["0,0", "1,0"]);
|
||||||
|
|
||||||
|
const result = validateShapePlacement(
|
||||||
|
shape,
|
||||||
|
IDENTITY_TRANSFORM,
|
||||||
|
10,
|
||||||
|
10,
|
||||||
|
occupied,
|
||||||
|
);
|
||||||
|
expect(result).toEqual({ valid: false, reason: "与已有形状重叠" });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("transformShape", () => {
|
||||||
|
it("should apply translation correctly", () => {
|
||||||
|
const shape = parseShapeString("o");
|
||||||
|
const transform = { ...IDENTITY_TRANSFORM, offset: { x: 5, y: 3 } };
|
||||||
|
|
||||||
|
const cells = transformShape(shape, transform);
|
||||||
|
expect(cells).toEqual([{ x: 5, y: 3 }]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should apply 90° rotation correctly", () => {
|
||||||
|
const shape = parseShapeString("oe");
|
||||||
|
const transform = { ...IDENTITY_TRANSFORM, rotation: 90 };
|
||||||
|
|
||||||
|
const cells = transformShape(shape, transform);
|
||||||
|
expect(cells).toEqual([
|
||||||
|
{ x: 0, y: 0 },
|
||||||
|
{ x: 0, y: -1 },
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should apply horizontal flip correctly", () => {
|
||||||
|
const shape = parseShapeString("oe");
|
||||||
|
const transform = { ...IDENTITY_TRANSFORM, flipX: true };
|
||||||
|
|
||||||
|
const cells = transformShape(shape, transform);
|
||||||
|
expect(cells).toEqual([
|
||||||
|
{ x: 1, y: 0 },
|
||||||
|
{ x: 0, y: 0 },
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should apply vertical flip correctly", () => {
|
||||||
|
const shape = parseShapeString("os");
|
||||||
|
const transform = { ...IDENTITY_TRANSFORM, flipY: true };
|
||||||
|
|
||||||
|
const cells = transformShape(shape, transform);
|
||||||
|
expect(cells).toEqual([
|
||||||
|
{ x: 0, y: 1 },
|
||||||
|
{ x: 0, y: 0 },
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should combine rotation and translation", () => {
|
||||||
|
const shape = parseShapeString("os");
|
||||||
|
const transform = {
|
||||||
|
...IDENTITY_TRANSFORM,
|
||||||
|
rotation: 90,
|
||||||
|
offset: { x: 10, y: 10 },
|
||||||
|
};
|
||||||
|
|
||||||
|
const cells = transformShape(shape, transform);
|
||||||
|
expect(cells).toEqual([
|
||||||
|
{ x: 10, y: 10 },
|
||||||
|
{ x: 11, y: 10 },
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue