Skip to content

Instantly share code, notes, and snippets.

@mmazzarolo
Last active November 10, 2019 14:38
Show Gist options
  • Save mmazzarolo/bfb092966a404231d4d77c3c8bc0623a to your computer and use it in GitHub Desktop.
Save mmazzarolo/bfb092966a404231d4d77c3c8bc0623a to your computer and use it in GitHub Desktop.
Puzzle Game logic
import {
LayoutRectangle,
LayoutChangeEvent,
GestureResponderEvent
} from "react-native";
import { createContext, useContext } from "react";
import { observable, action, computed } from "mobx";
import { takeWhile, takeRightWhile, sortBy, intersectionBy } from "lodash";
const isOppositeDirectionOf = (dir1: string, dir2: string) => {
return (
(dir1 === "right" && dir2 === "left") ||
(dir1 === "left" && dir2 === "right") ||
(dir1 === "top" && dir2 === "bottom") ||
(dir1 === "bottom" && dir2 === "top")
);
};
export class Cell {
root: RootStore;
row: number;
col: number;
value: string;
constructor(root: RootStore, row: number, col: number, value: string) {
this.root = root;
this.row = row;
this.col = col;
this.value = value;
}
@computed
get id() {
return `${this.row}:${this.col}`;
}
@computed
get completed() {
const line = this.root.board.lineOf(this);
if (!line) return false;
return line.completed;
}
@computed
get valid() {
const line = this.root.board.lineOf(this);
if (!line) return false;
return line.valid;
}
@computed
get orientation() {
const line = this.root.board.lineOf(this);
if (!line) return "none";
if (line.orientation === "initial") {
return line.origin.equals(this) ? "single" : "none";
} else if (line.orientation === "horizontal") {
const edgeCols = [line.edges[0].col, line.edges[1].col].sort();
const [leftEdgeCol, rightEdgeCol] = edgeCols;
if (leftEdgeCol === this.col) {
return "horizontal-left";
} else if (rightEdgeCol === this.col) {
return "horizontal-right";
} else {
return "horizontal-middle";
}
} else if (line.orientation === "vertical") {
const edgeRows = [line.edges[0].row, line.edges[1].row].sort();
const [topEdgeRow, bottomEdgeRow] = edgeRows;
if (topEdgeRow === this.row) {
return "vertical-top";
} else if (bottomEdgeRow === this.row) {
return "vertical-bottom";
} else {
return "vertical-middle";
}
}
}
@computed
get hovered() {
return this.equals(this.root.interactions.hoveredCell);
}
equals(cell?: Cell | null) {
return cell ? this.id === cell.id : false;
}
onSameRowOf(cell: Cell) {
return this.row === cell.row;
}
onSameColumnOf(cell: Cell) {
return this.col === cell.col;
}
onLeftOf(cell: Cell) {
return this.onSameRowOf(cell) && this.col <= cell.col;
}
onRightOf(cell: Cell) {
return this.onSameRowOf(cell) && this.col >= cell.col;
}
onTopOf(cell: Cell) {
return this.onSameColumnOf(cell) && this.row <= cell.row;
}
onBottomOf(cell: Cell) {
return this.onSameColumnOf(cell) && this.row >= cell.row;
}
horizontallyAdjacentTo(cell: Cell) {
return this.col + 1 === cell.col || this.col - 1 === cell.col;
}
verticallyAdjacentTo(cell: Cell) {
return this.row + 1 === cell.row || this.row - 1 === cell.row;
}
}
class Line {
@observable origin: Cell;
@observable committedCells = observable.array<Cell>([]);
@observable pendingCells = observable.array<Cell>([]);
@observable stale: boolean;
@observable currentHandler: Cell | null;
constructor(origin: Cell) {
this.origin = origin;
this.committedCells.replace([origin]);
this.pendingCells.replace([]);
this.stale = false;
this.currentHandler = null;
}
@computed
get id() {
return this.origin.id;
}
@computed
get orientation() {
if (!this.cells.length) {
throw new Error("Line.orientation » Lines with no cells set");
} else if (this.cells.length === 1) {
return "initial";
} else if (this.cells[0].onSameRowOf(this.cells[1])) {
return "horizontal";
} else {
return "vertical";
}
}
@computed
get edges() {
if (this.orientation === "initial") {
return [this.cells[0], this.cells[0]];
} else if (this.orientation === "horizontal") {
const cellsByCols = sortBy(this.cells.slice(), cell => cell.col);
return [cellsByCols[0], cellsByCols[cellsByCols.length - 1]];
} else {
const cellsByRows = sortBy(this.cells.slice(), cell => cell.row);
return [cellsByRows[0], cellsByRows[cellsByRows.length - 1]];
}
}
@computed
get valid() {
return Number(this.origin.value) >= this.cells.length;
}
@computed
get completed() {
return Number(this.origin.value) === this.cells.length;
}
@computed
get leftCommittedCells() {
return this.committedCells.filter(cell => cell.onLeftOf(this.origin));
}
@computed
get rightCommittedCells() {
return this.committedCells.filter(cell => cell.onRightOf(this.origin));
}
@computed
get topCommittedCells() {
return this.committedCells.filter(cell => cell.onTopOf(this.origin));
}
@computed
get bottomCommittedCells() {
return this.committedCells.filter(cell => cell.onBottomOf(this.origin));
}
@computed
get pendingCellsDirection() {
if (this.pendingCells.length) {
if (this.pendingCells[0].onSameRowOf(this.origin)) {
return this.pendingCells[0].col > this.origin.col ? "right" : "left";
}
if (this.pendingCells[0].onSameColumnOf(this.origin)) {
return this.pendingCells[0].row > this.origin.row ? "bottom" : "top";
}
}
return "none";
}
@computed
get draggedDirection() {
if (!this.currentHandler) return "none";
if (this.currentHandler.equals(this.origin)) return "origin";
if (this.currentHandler) {
if (this.currentHandler.onSameRowOf(this.origin)) {
return this.currentHandler.col > this.origin.col ? "right" : "left";
}
if (this.currentHandler.onSameColumnOf(this.origin)) {
return this.currentHandler.row > this.origin.row ? "bottom" : "top";
}
}
return "none";
}
@computed
get cells() {
if (this.origin.equals(this.currentHandler)) {
if (
!this.pendingCells.length ||
intersectionBy(this.pendingCells, this.committedCells, "id").length
) {
return this.committedCells;
}
}
const isOpp = isOppositeDirectionOf(
this.draggedDirection,
this.pendingCellsDirection
);
if (isOpp) {
if (this.draggedDirection === "left") {
return this.rightCommittedCells;
} else if (this.draggedDirection === "right") {
return this.leftCommittedCells;
} else if (this.draggedDirection === "top") {
return this.bottomCommittedCells;
} else if (this.draggedDirection === "bottom") {
return this.topCommittedCells;
}
}
if (this.pendingCellsDirection === "left") {
return this.rightCommittedCells.concat(this.pendingCells);
} else if (this.pendingCellsDirection === "right") {
return this.leftCommittedCells.concat(this.pendingCells);
} else if (this.pendingCellsDirection === "top") {
return this.bottomCommittedCells.concat(this.pendingCells);
} else if (this.pendingCellsDirection === "bottom") {
return this.topCommittedCells.concat(this.pendingCells);
}
if (this.stale) {
if (this.draggedDirection === "left") {
return this.rightCommittedCells;
} else if (this.draggedDirection === "right") {
return this.leftCommittedCells;
} else if (this.draggedDirection === "top") {
return this.bottomCommittedCells;
} else if (this.draggedDirection === "bottom") {
return this.topCommittedCells;
}
return this.pendingCells.concat([this.origin]);
}
return this.committedCells;
}
@action
stalify(currentHandler: Cell) {
this.currentHandler = currentHandler;
}
@action
replacePendingCells(cells: Cell[]) {
if (!this.stale) {
this.stale = true;
}
this.pendingCells.replace(cells.filter(cell => !cell.equals(this.origin)));
}
@action
commit() {
this.committedCells.replace(this.cells.slice());
this.pendingCells.clear();
this.stale = false;
}
@action
reset() {
this.committedCells.replace([this.origin]);
this.pendingCells.clear();
}
contains(cell: Cell) {
return this.cells.find(cell.equals);
}
isEdge(cell: Cell) {
return this.edges[0].equals(cell) || this.edges[1].equals(cell);
}
equals(line?: Line) {
return line && this.id === line.id;
}
}
class BoardStore {
root: RootStore;
@observable grid = observable<Cell[]>([]);
@observable lines = observable<Line>([]);
constructor(rootStore: RootStore) {
this.root = rootStore;
}
@action
initialize(rows: string[]) {
const grid: Cell[][] = [];
const lines: Line[] = [];
rows.forEach((rowString: string, row: number) => {
grid.push([]);
rowString.split("").forEach((value: string, col: number) => {
const cell = new Cell(this.root, row, col, value);
if (Number(value) > 0 && Number(value) <= 9) {
const line = new Line(cell);
lines.push(line);
}
grid[row].push(cell);
});
});
this.grid.replace(grid);
this.lines.replace(lines);
}
@action
reset() {
this.lines.forEach(line => {
line.reset();
});
}
@computed
get isInitialized() {
return !!this.grid.length;
}
@computed
get rowsCount() {
return this.grid.length;
}
@computed
get colsCount() {
return this.grid?.[0]?.length;
}
@computed
get cleared() {
if (this.lines.find(line => !line.completed)) {
return false;
}
const hasEmptyDot = this.grid.some(row => {
return row.some(cell => {
if (cell.value === ".") {
return !cell.valid || !cell.completed;
}
});
});
if (hasEmptyDot) {
return false;
}
return true;
}
@action
fillLine(line: Line, from: Cell, to: Cell) {
const cells = this.cellsBetween(line, from, to);
line.replacePendingCells(cells);
}
cellsBetween(line: Line, from: Cell, to: Cell) {
const shouldDrop = (cell: Cell) => {
return !this.lineOf(cell) || line.equals(this.lineOf(cell));
};
if (from.onSameRowOf(to)) {
const row = from.row;
const [colStart, colEnd] = [from, to].map(cell => cell.col).sort();
const cellsInBetween = [];
for (let col = colStart; col <= colEnd; col++) {
cellsInBetween.push(this.at(row, col));
}
return from.col < to.col
? takeWhile(cellsInBetween, shouldDrop)
: takeRightWhile(cellsInBetween, shouldDrop);
} else if (from.onSameColumnOf(to)) {
const col = from.col;
const [rowStart, rowEnd] = [from, to].map(cell => cell.row).sort();
const cellsInBetween = [];
for (let row = rowStart; row <= rowEnd; row++) {
cellsInBetween.push(this.at(row, col));
}
return from.row < to.row
? takeWhile(cellsInBetween, shouldDrop)
: takeRightWhile(cellsInBetween, shouldDrop);
}
return [];
}
at(row: number, col: number) {
return this.grid[row][col];
}
lineOf(cell: Cell) {
return this.lines.find(line => {
return !!line.cells.find(lineCell => {
return lineCell.equals(cell);
});
});
}
isCellInLine(cell: Cell) {
return !!this.lineOf(cell);
}
}
class InteractionsStore {
root: RootStore;
gridLayout?: LayoutRectangle;
@observable currentHandler?: Cell;
@observable draggedLine?: Line;
@observable hoveredCell?: Cell;
@observable numberOfMoves: number = 0;
constructor(rootStore: RootStore) {
this.root = rootStore;
}
@computed
get isDragging() {
return !!this.currentHandler;
}
/* ===================
* REACT NATIVE EVENTS
* =================== */
findCell(event: GestureResponderEvent) {
if (!this.gridLayout) return;
const cellWidth = this.gridLayout.width / this.root.board.colsCount;
const cellHeight = this.gridLayout.height / this.root.board.rowsCount;
const row = Math.floor(event.nativeEvent.locationY / cellHeight);
const col = Math.floor(event.nativeEvent.locationX / cellWidth);
const cell = this.root.board.at(row, col);
return cell;
}
isOutsideGrid(event: GestureResponderEvent) {
if (!this.gridLayout) return true;
return (
event.nativeEvent.locationY <= 0 ||
event.nativeEvent.locationY >= this.gridLayout.height ||
event.nativeEvent.locationX <= 0 ||
event.nativeEvent.locationX >= this.gridLayout.width
);
}
setGridLayout(layoutChangeEvent: LayoutChangeEvent) {
this.gridLayout = layoutChangeEvent.nativeEvent.layout;
}
onGridTouchStart(event: GestureResponderEvent) {
if (this.isOutsideGrid(event)) {
this.onGridTouchExit();
return;
}
const cell = this.findCell(event);
if (!cell) return;
this.onCellTouch(cell);
}
onGridTouchMove(event: GestureResponderEvent) {
if (!this.isDragging) return;
if (this.isOutsideGrid(event)) {
this.onGridTouchExit();
return;
}
const cell = this.findCell(event);
if (!cell) return;
const isNewCell = !cell.equals(this.hoveredCell);
if (isNewCell) {
if (this.hoveredCell) {
this.onCellLeave(this.hoveredCell);
}
this.onCellEnter(cell);
}
}
onGridTouchEnd(event: GestureResponderEvent) {
if (!this.isDragging) return;
if (this.isOutsideGrid(event)) {
this.onGridTouchExit();
return;
}
const cell = this.findCell(event);
if (!cell) return;
this.onCellTouchEnd(cell);
}
/* ===================
* COMMON HANDLERS
* =================== */
@action
onCellTouch(cell: Cell) {
const line = this.root.board.lineOf(cell);
if (!line) return;
const isValidHandler = cell.equals(line.origin) || line.isEdge(cell);
if (isValidHandler) {
this.currentHandler = cell;
this.draggedLine = line;
this.hoveredCell = cell;
}
line.stalify(cell);
}
@action
onCellEnter(cell: Cell) {
this.numberOfMoves++;
if (!this.currentHandler) return;
if (!this.draggedLine) return;
if (
!this.draggedLine.origin.onSameColumnOf(cell) &&
!this.draggedLine.origin.onSameRowOf(cell)
)
return;
this.hoveredCell = cell;
this.root.board.fillLine(this.draggedLine, this.draggedLine.origin, cell);
}
@action
onCellLeave(cell: Cell) {
if (!this.currentHandler || !this.draggedLine) return;
if (
cell.equals(this.currentHandler) &&
cell.equals(this.draggedLine.origin) &&
!this.draggedLine.isEdge(cell)
) {
this.draggedLine.reset();
}
}
@action
onCellTouchEnd(cell: Cell) {
if (!this.draggedLine) return;
const isOrigin = cell.equals(this.draggedLine.origin);
const isTap = this.numberOfMoves < 2;
if (isOrigin && isTap) {
this.draggedLine.reset();
}
if (this.draggedLine) {
this.draggedLine.commit();
}
this.currentHandler = undefined;
this.draggedLine = undefined;
this.hoveredCell = undefined;
this.numberOfMoves = 0;
}
@action
onGridTouchExit() {
this.currentHandler = undefined;
this.draggedLine = undefined;
this.hoveredCell = undefined;
this.numberOfMoves = 0;
}
}
class RootStore {
board: BoardStore;
interactions: InteractionsStore;
constructor() {
this.board = new BoardStore(this);
this.interactions = new InteractionsStore(this);
}
}
export const rootStore = new RootStore();
export const storesContext = createContext({
board: rootStore.board,
interactions: rootStore.interactions
});
export const useBoardStores = () => useContext(storesContext);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment