refactor: reorg
This commit is contained in:
@@ -0,0 +1,109 @@
|
||||
/**
|
||||
* Parsed shape result containing the grid and sizing information.
|
||||
*/
|
||||
export interface ParsedShape {
|
||||
/** 2D boolean grid representing filled cells */
|
||||
grid: boolean[][];
|
||||
/** Grid width (number of columns) */
|
||||
width: number;
|
||||
/** Grid height (number of rows) */
|
||||
height: number;
|
||||
/** Number of occupied (filled) cells */
|
||||
count: number;
|
||||
/** Origin X coordinate within the grid */
|
||||
originX: number;
|
||||
/** Origin Y coordinate within the grid */
|
||||
originY: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses a movement string into a 2D boolean grid.
|
||||
* Rules:
|
||||
* o: Fill current cell and set origin
|
||||
* n, s, e, w: Move one step and fill the cell
|
||||
* r: Return to the previous cell
|
||||
*/
|
||||
export function parseShapeString(input: string): ParsedShape {
|
||||
let curX = 0;
|
||||
let curY = 0;
|
||||
let originX = 0;
|
||||
let originY = 0;
|
||||
let originSet = false;
|
||||
|
||||
// Track unique filled coordinates
|
||||
const filledPoints = new Set<string>();
|
||||
// Stack to track movement history for the 'r' command
|
||||
const history: { x: number; y: number }[] = [];
|
||||
|
||||
const fill = (x: number, y: number) => {
|
||||
filledPoints.add(`${x},${y}`);
|
||||
history.push({ x, y });
|
||||
};
|
||||
|
||||
for (const char of input.toLowerCase()) {
|
||||
switch (char) {
|
||||
case 'o':
|
||||
if (!originSet) {
|
||||
originX = curX;
|
||||
originY = curY;
|
||||
originSet = true;
|
||||
}
|
||||
fill(curX, curY);
|
||||
break;
|
||||
case 'n':
|
||||
curY -= 1;
|
||||
fill(curX, curY);
|
||||
break;
|
||||
case 's':
|
||||
curY += 1;
|
||||
fill(curX, curY);
|
||||
break;
|
||||
case 'e':
|
||||
curX += 1;
|
||||
fill(curX, curY);
|
||||
break;
|
||||
case 'w':
|
||||
curX -= 1;
|
||||
fill(curX, curY);
|
||||
break;
|
||||
case 'r':
|
||||
if (history.length > 1) {
|
||||
history.pop(); // Remove current position
|
||||
const prev = history[history.length - 1];
|
||||
curX = prev.x;
|
||||
curY = prev.y;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (filledPoints.size === 0) {
|
||||
return { grid: [[]], width: 0, height: 1, count: 0, originX: 0, originY: 0 };
|
||||
}
|
||||
|
||||
// Calculate bounding box to normalize the array
|
||||
const coords = Array.from(filledPoints).map(p => p.split(',').map(Number));
|
||||
const minX = Math.min(...coords.map(c => c[0]));
|
||||
const maxX = Math.max(...coords.map(c => c[0]));
|
||||
const minY = Math.min(...coords.map(c => c[1]));
|
||||
const maxY = Math.max(...coords.map(c => c[1]));
|
||||
|
||||
const width = maxX - minX + 1;
|
||||
const height = maxY - minY + 1;
|
||||
|
||||
// Initialize the grid
|
||||
const grid: boolean[][] = Array.from({ length: height }, () =>
|
||||
Array(width).fill(false)
|
||||
);
|
||||
|
||||
// Map the points into the grid using offsets
|
||||
for (const [x, y] of coords) {
|
||||
grid[y - minY][x - minX] = true;
|
||||
}
|
||||
|
||||
// Normalize origin coordinates relative to the grid
|
||||
const normalizedOriginX = originX - minX;
|
||||
const normalizedOriginY = originY - minY;
|
||||
|
||||
return { grid, width, height, count: filledPoints.size, originX: normalizedOriginX, originY: normalizedOriginY };
|
||||
}
|
||||
@@ -0,0 +1,227 @@
|
||||
import type { ParsedShape } from './parse-shape';
|
||||
|
||||
/**
|
||||
* Represents a 2D point in grid coordinates.
|
||||
*/
|
||||
export interface Point2D {
|
||||
x: number;
|
||||
y: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 2D transformation to apply to a shape.
|
||||
*/
|
||||
export interface Transform2D {
|
||||
/** Translation offset in grid units */
|
||||
offset: Point2D;
|
||||
/** Rotation in degrees (0, 90, 180, 270) */
|
||||
rotation: number;
|
||||
/** Whether to flip horizontally */
|
||||
flipX: boolean;
|
||||
/** Whether to flip vertically */
|
||||
flipY: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Default transform (identity).
|
||||
*/
|
||||
export const IDENTITY_TRANSFORM: Transform2D = {
|
||||
offset: { x: 0, y: 0 },
|
||||
rotation: 0,
|
||||
flipX: false,
|
||||
flipY: false,
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets all occupied cell coordinates from a shape.
|
||||
*/
|
||||
export function getOccupiedCells(shape: ParsedShape): Point2D[] {
|
||||
const cells: Point2D[] = [];
|
||||
for (let y = 0; y < shape.height; y++) {
|
||||
for (let x = 0; x < shape.width; x++) {
|
||||
if (shape.grid[y]?.[x]) {
|
||||
cells.push({ x, y });
|
||||
}
|
||||
}
|
||||
}
|
||||
return cells;
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies a 2D transformation to a point.
|
||||
*/
|
||||
export function transformPoint(
|
||||
point: Point2D,
|
||||
transform: Transform2D,
|
||||
shapeWidth: number,
|
||||
shapeHeight: number
|
||||
): Point2D {
|
||||
let { x, y } = point;
|
||||
|
||||
// Apply flips
|
||||
if (transform.flipX) {
|
||||
x = shapeWidth - 1 - x;
|
||||
}
|
||||
if (transform.flipY) {
|
||||
y = shapeHeight - 1 - y;
|
||||
}
|
||||
|
||||
// Apply rotation (around origin 0,0)
|
||||
const rotation = ((transform.rotation % 360) + 360) % 360;
|
||||
let rotatedX = x;
|
||||
let rotatedY = y;
|
||||
|
||||
switch (rotation) {
|
||||
case 90:
|
||||
rotatedX = y;
|
||||
rotatedY = -x;
|
||||
break;
|
||||
case 180:
|
||||
rotatedX = -x;
|
||||
rotatedY = -y;
|
||||
break;
|
||||
case 270:
|
||||
rotatedX = -y;
|
||||
rotatedY = x;
|
||||
break;
|
||||
}
|
||||
|
||||
// Apply offset
|
||||
return {
|
||||
x: rotatedX + transform.offset.x,
|
||||
y: rotatedY + transform.offset.y,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Transforms a shape and returnss its occupied cells in world coordinates.
|
||||
*/
|
||||
export function transformShape(shape: ParsedShape, transform: Transform2D): 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).
|
||||
*/
|
||||
export function checkCollision(
|
||||
shapeA: ParsedShape,
|
||||
transformA: Transform2D,
|
||||
shapeB: ParsedShape,
|
||||
transformB: Transform2D
|
||||
): boolean {
|
||||
const cellsA = transformShape(shapeA, transformA);
|
||||
const cellsB = transformShape(shapeB, transformB);
|
||||
|
||||
const setA = new Set(cellsA.map(c => `${c.x},${c.y}`));
|
||||
|
||||
for (const cell of cellsB) {
|
||||
if (setA.has(`${cell.x},${cell.y}`)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a transformed shape collides with any occupied cells on a board.
|
||||
* @param shape The shape to check
|
||||
* @param transform The transform to apply to the shape
|
||||
* @param occupiedCells Set of occupied board cells in "x,y" format
|
||||
*/
|
||||
export function checkBoardCollision(
|
||||
shape: ParsedShape,
|
||||
transform: Transform2D,
|
||||
occupiedCells: Set<string>
|
||||
): boolean {
|
||||
const cells = transformShape(shape, transform);
|
||||
|
||||
for (const cell of cells) {
|
||||
if (occupiedCells.has(`${cell.x},${cell.y}`)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a transformed shape is within bounds.
|
||||
* @param shape The shape to check
|
||||
* @param transform The transform to apply to the shape
|
||||
* @param boardWidth Board width
|
||||
* @param boardHeight Board height
|
||||
*/
|
||||
export function checkBounds(
|
||||
shape: ParsedShape,
|
||||
transform: Transform2D,
|
||||
boardWidth: number,
|
||||
boardHeight: number
|
||||
): boolean {
|
||||
const cells = transformShape(shape, transform);
|
||||
|
||||
for (const cell of cells) {
|
||||
if (cell.x < 0 || cell.x >= boardWidth || cell.y < 0 || cell.y >= boardHeight) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates that a placement is both in bounds and collision-free.
|
||||
* @returns Object with `valid` flag and optional `reason` string
|
||||
*/
|
||||
export function validatePlacement(
|
||||
shape: ParsedShape,
|
||||
transform: Transform2D,
|
||||
boardWidth: number,
|
||||
boardHeight: number,
|
||||
occupiedCells: Set<string>
|
||||
): { valid: true } | { valid: false; reason: string } {
|
||||
if (!checkBounds(shape, transform, boardWidth, boardHeight)) {
|
||||
return { valid: false, reason: '超出边界' };
|
||||
}
|
||||
|
||||
if (checkBoardCollision(shape, transform, occupiedCells)) {
|
||||
return { valid: false, reason: '与已有形状重叠' };
|
||||
}
|
||||
|
||||
return { valid: true };
|
||||
}
|
||||
|
||||
/**
|
||||
* Rotates a transform by the given degrees.
|
||||
* @param current The current transform
|
||||
* @param degrees Degrees to rotate (typically 90, 180, or 270)
|
||||
*/
|
||||
export function rotateTransform(current: Transform2D, degrees: number): Transform2D {
|
||||
return {
|
||||
...current,
|
||||
rotation: ((current.rotation + degrees) % 360 + 360) % 360,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Flips a transform horizontally.
|
||||
*/
|
||||
export function flipXTransform(current: Transform2D): Transform2D {
|
||||
return {
|
||||
...current,
|
||||
flipX: !current.flipX,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Flips a transform vertically.
|
||||
*/
|
||||
export function flipYTransform(current: Transform2D): Transform2D {
|
||||
return {
|
||||
...current,
|
||||
flipY: !current.flipY,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user