Last active
November 10, 2019 14:38
-
-
Save mmazzarolo/bfb092966a404231d4d77c3c8bc0623a to your computer and use it in GitHub Desktop.
Puzzle Game logic
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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