This commit is contained in:
2026-06-26 20:32:33 -04:00
commit 48bf9edb65
146 changed files with 19805 additions and 0 deletions
+11
View File
@@ -0,0 +1,11 @@
import { GameContextProvider, GameScreen } from "./internal";
const App = () => {
return (
<GameContextProvider>
<GameScreen />
</GameContextProvider>
);
};
export default App;
+28
View File
@@ -0,0 +1,28 @@
import { nanoid } from "nanoid";
import { CellSetupConfig } from "../../internal";
class Cell {
constructor({ canvasX, canvasY, x, y }: CellSetupConfig) {
this.canvasX = canvasX;
this.canvasY = canvasY;
this.x = x;
this.y = y;
this.id = nanoid();
}
canvasX: number;
canvasY: number;
config: { [key: string]: any };
currentColor: string;
id: string;
nextColor: string;
x: number;
y: number;
setCurrentColor(newColor: string) {
this.currentColor = newColor;
}
setNextColor(newColor: string) {
this.nextColor = newColor;
}
}
export default Cell;
+4
View File
@@ -0,0 +1,4 @@
import Cell from "./Cell";
export * from "./types";
export { Cell };
+6
View File
@@ -0,0 +1,6 @@
export interface CellSetupConfig {
canvasX: number;
canvasY: number;
x: number;
y: number;
}
+75
View File
@@ -0,0 +1,75 @@
import {
Cell,
clearCanvas,
createHexRows,
createSquareRows,
drawHexagon,
drawSquare,
getNeighbors,
} from "../../internal";
import { GridConfig } from "./types";
class Grid {
constructor(config: GridConfig) {
this.config = config;
const { cellShape, loops } = this.config;
this.setupCells();
this.getNeighbors = (cell: Cell) =>
getNeighbors({ cellShape, loops, rows: this.rows }, cell);
this.init();
}
config: GridConfig;
ctx: CanvasRenderingContext2D = null;
rows: Cell[][] = [];
getNeighbors: (cell: Cell) => Cell[];
init() {
this.config.ruleset.init(this);
this.render();
}
iterateCells(fn: (cell: Cell, x?: number, y?: number) => void) {
this.rows.forEach((row, x) => row.forEach((cell, y) => fn(cell, x, y)));
}
render() {
const cells: Cell[] = [];
this.iterateCells((cell) => {
cells.push(cell);
});
// sorting by color improves performance by reducing the number of times the fillStyle changes
const sortedCells = [...cells].sort((a, b) =>
a.currentColor > b.currentColor ? 1 : -1
);
sortedCells.forEach((cell) => {
switch (this.config.cellShape) {
case "hex":
drawHexagon(cell, this.ctx, this.config.cellSize);
break;
case "square":
drawSquare(cell, this.ctx, this.config.cellSize);
break;
}
});
}
reset() {
clearCanvas();
this.setupCells();
this.init();
}
setupCells() {
let rows = [];
switch (this.config.cellShape) {
case "hex":
rows = createHexRows(this.config);
break;
case "square":
rows = createSquareRows(this.config);
break;
}
this.rows = rows;
}
update() {
this.config.ruleset.update(this);
this.render();
}
}
export default Grid;
+4
View File
@@ -0,0 +1,4 @@
import Grid from "./Grid";
export * from "./types";
export { Grid };
+12
View File
@@ -0,0 +1,12 @@
import { Ruleset } from "../../internal";
export type CellShape = "hex" | "square";
export interface GridConfig {
cellShape: CellShape;
cellSize: number;
height: number;
loops: boolean;
ruleset: Ruleset;
width: number;
}
+14
View File
@@ -0,0 +1,14 @@
import { GlobalConfig } from "../../context";
import { GameSpecificConfig } from "./types";
abstract class Ruleset {
constructor(config?: GameSpecificConfig) {
this.config = config;
}
config?: GameSpecificConfig;
defaultGlobalSettings?: GlobalConfig;
init: (arg0: any) => void;
update: (arg0: any) => void;
}
export default Ruleset;
+4
View File
@@ -0,0 +1,4 @@
import Ruleset from "./Ruleset";
export * from "./types";
export { Ruleset };
+1
View File
@@ -0,0 +1 @@
export type GameSpecificConfig = { [key: string]: any };
+3
View File
@@ -0,0 +1,3 @@
export * from "./Grid";
export * from "./Ruleset";
export * from "./Cell";
+40
View File
@@ -0,0 +1,40 @@
import { CanvasProps } from "./types";
import { useContext, useEffect } from "react";
import {
GameContext,
getCanvasSize,
getContext,
useCanvas,
} from "../../internal";
const Canvas = (props: CanvasProps) => {
const { canvasAttributes } = props;
const { currentGame } = useContext(GameContext);
const { grid, globalConfigs } = useContext(GameContext);
const { throttleAmount } = globalConfigs[currentGame];
useEffect(() => {
if (grid && grid.ctx === null) {
grid.ctx = getContext();
}
}, [grid]);
const draw = (frameCount: number) => {
if (frameCount % throttleAmount !== 0) return;
grid?.update();
};
const canvasRef = useCanvas(draw);
const { height, width } = getCanvasSize();
return (
<canvas
ref={canvasRef}
height={height}
width={width}
{...canvasAttributes}
/>
);
};
export default Canvas;
+3
View File
@@ -0,0 +1,3 @@
import Canvas from "./Canvas";
export * from "./types";
export { Canvas };
+5
View File
@@ -0,0 +1,5 @@
import { AllHTMLAttributes } from "react";
export interface CanvasProps {
canvasAttributes: AllHTMLAttributes<HTMLCanvasElement>;
}
+89
View File
@@ -0,0 +1,89 @@
import { useContext } from "react";
import {
Box,
Button,
Divider,
Grid,
IconButton,
Typography,
} from "@mui/material";
import CloseIcon from "@mui/icons-material/Close";
import { GameSpecificControls } from "./GameSpecificControls";
import { GlobalControls } from "./GlobalControls";
import { ControlsProps } from "./types";
import { ControlsContext } from "./context";
import { GameContext, GlobalConfig } from "../../context";
const Controls = ({ closeDrawer }: ControlsProps) => {
const { currentGame } = useContext(GameContext);
const {
globalConfigsTouched,
newGlobalConfigs,
setGlobalConfigsTouched,
setNewGlobalConfigs,
start,
} = useContext(ControlsContext);
return (
<Box p={5}>
<Grid container alignItems="center" justifyContent="flex-end">
<Grid item>
<IconButton onClick={closeDrawer} size="large">
<CloseIcon />
</IconButton>
</Grid>
</Grid>
<Grid container spacing={2}>
<Grid
container
item
md={6}
xs={12}
spacing={2}
alignContent="flex-start"
>
<GlobalControls
config={newGlobalConfigs[currentGame]}
setConfig={(newConfig: GlobalConfig) => {
setNewGlobalConfigs({
...newGlobalConfigs,
[currentGame]: newConfig,
});
setGlobalConfigsTouched({
...globalConfigsTouched,
[currentGame]: true,
});
}}
/>
</Grid>
<Grid
container
item
md={6}
xs={12}
spacing={2}
alignContent="flex-start"
>
<Grid item xs={12}>
<Typography>Game-Specific Settings</Typography>
<Divider />
</Grid>
<GameSpecificControls />
</Grid>
<Grid item xs={12}>
<Button
fullWidth
onClick={() => {
start(closeDrawer);
}}
variant="contained"
>
Start!
</Button>
</Grid>
</Grid>
</Box>
);
};
export default Controls;
@@ -0,0 +1,275 @@
import { useContext } from "react";
import {
Grid,
TextField,
MenuItem,
FormControl,
FormLabel,
FormGroup,
FormControlLabel,
Checkbox,
} from "@mui/material";
import { MuiColorInput } from "mui-color-input";
import {
ConwayConfig,
GameContext,
Preset,
RulesetName,
} from "../../../../internal";
import { presetDisplayNameMap } from "./const";
import { ConwayColorProp } from "./types";
import { useGameSpecificOptions } from "../hooks";
const conwayColorProps = [
"liveColor",
"deadColor",
"startingEnvelopeColor",
"finalEnvelopeColor",
] as ConwayColorProp[];
const ConwayControls = () => {
const { grid } = useContext(GameContext);
const { config, handleUpdate } = useGameSpecificOptions<ConwayConfig>(
RulesetName.CONWAY
);
const neighborNumbers =
grid.config.cellShape === "hex"
? [0, 1, 2, 3, 4, 5, 6]
: [0, 1, 2, 3, 4, 5, 6, 7, 8];
return (
<>
<Grid item xs={12}>
<TextField
fullWidth
label="Preset"
onChange={(e) => {
handleUpdate({ ...config, preset: e.target.value as Preset });
}}
select
value={config.preset}
>
{Object.keys(Preset).map((preset) => (
<MenuItem key={preset} value={Preset[preset]}>
{presetDisplayNameMap[Preset[preset]]}
</MenuItem>
))}
</TextField>
</Grid>
<Grid item xs={12}>
<TextField
fullWidth
label="Average % Alive at Start"
type="number"
onChange={(e) =>
handleUpdate({
...config,
liveStartPercent: parseInt(e.target.value),
})
}
value={config.liveStartPercent}
/>
</Grid>
<Grid item xs={12}>
<FormControl component="fieldset" variant="standard">
<FormLabel component="legend">
Live Neighbors Required (Survival)
</FormLabel>
<FormGroup row>
{neighborNumbers.map((num) => (
<FormControlLabel
control={
<Checkbox
checked={config.neighborsNeededToSurvive.includes(num)}
onChange={(e) => {
let newNeighborsNeededToSurvive = [
...config.neighborsNeededToSurvive,
];
if (e.target.checked) {
newNeighborsNeededToSurvive.push(num);
} else {
newNeighborsNeededToSurvive =
newNeighborsNeededToSurvive.filter(
(val) => val !== num
);
}
handleUpdate({
...config,
neighborsNeededToSurvive: newNeighborsNeededToSurvive,
});
}}
name={num.toString()}
/>
}
label={num.toString()}
key={`conway-survival-${num}`}
/>
))}
</FormGroup>
</FormControl>
</Grid>
<Grid item xs={12}>
<FormControl component="fieldset" variant="standard">
<FormLabel component="legend">
Live Neighbors Required (Reproduction)
</FormLabel>
<FormGroup row>
{neighborNumbers.map((num) => (
<FormControlLabel
control={
<Checkbox
checked={config.neighborsNeededToReproduce.includes(num)}
onChange={(e) => {
let newNeighborsNeededToReproduce = [
...config.neighborsNeededToReproduce,
];
if (e.target.checked) {
newNeighborsNeededToReproduce.push(num);
} else {
newNeighborsNeededToReproduce =
newNeighborsNeededToReproduce.filter(
(val) => val !== num
);
}
handleUpdate({
...config,
neighborsNeededToReproduce:
newNeighborsNeededToReproduce,
});
}}
name={num.toString()}
/>
}
label={num.toString()}
key={`conway-reproduction-${num}`}
/>
))}
</FormGroup>
</FormControl>
</Grid>
<Grid item md={6} xs={12}>
<FormGroup row>
<FormControlLabel
control={
<Checkbox
checked={config.showEnvelope}
onChange={(e) => {
handleUpdate({
...config,
showEnvelope: e.target.checked,
});
}}
name={"show-envelope-checkbox"}
/>
}
label={"Show Envelope"}
/>
{config.showEnvelope && (
<FormControlLabel
control={
<Checkbox
checked={config.showEnvelopeGradient}
onChange={(e) => {
handleUpdate({
...config,
showEnvelopeGradient: e.target.checked,
});
}}
name={"envelope-gradient-checkbox"}
/>
}
label={"Envelope Gradient"}
/>
)}
</FormGroup>
</Grid>
<Grid item md={6} xs={12}>
{config.showEnvelope && config.showEnvelopeGradient && (
<TextField
fullWidth
label="Envelope Gradient Steps"
type="number"
onChange={(e) =>
handleUpdate({
...config,
envelopeGradientSteps: parseInt(e.target.value),
})
}
value={config.envelopeGradientSteps}
/>
)}
</Grid>
<Grid container item xs={12} spacing={2}>
{conwayColorProps.map((color: ConwayColorProp) => {
if (
(!config.showEnvelope && color === "startingEnvelopeColor") ||
((!config.showEnvelope || !config.showEnvelopeGradient) &&
color === "finalEnvelopeColor")
)
return null;
let label = "Alive Color";
if (color === "deadColor") {
label = "Dead Color";
} else if (color === "startingEnvelopeColor") {
label = config.showEnvelopeGradient
? "Initial Envelope Color"
: "Envelope Color";
} else if (color === "finalEnvelopeColor") {
label = "Final Envelope Color";
}
return (
<Grid item key={`conway-color-config-${color}`}>
<MuiColorInput
format="hex"
label={label}
value={config[color]}
onChange={(newColor) =>
handleUpdate({
...config,
[color]: newColor,
})
}
/>
</Grid>
);
})}
</Grid>
<Grid item md={6} xs={12}>
<FormControlLabel
control={
<Checkbox
checked={config.mortalCells}
onChange={(e) => {
handleUpdate({
...config,
mortalCells: e.target.checked,
});
}}
name={"mortal-cells-checkbox"}
/>
}
label={"Mortal Cells (Cells Die After X Ticks)"}
/>
</Grid>
<Grid item md={6} xs={12}>
{config.mortalCells && (
<TextField
fullWidth
label="Cell Lifespan"
type="number"
onChange={(e) =>
handleUpdate({
...config,
cellLifespan: parseInt(e.target.value),
})
}
value={config.cellLifespan}
/>
)}
</Grid>
</>
);
};
export default ConwayControls;
@@ -0,0 +1,5 @@
import { Preset } from "../../../../internal";
export const presetDisplayNameMap = {
[Preset.DEFAULT]: "Random Distribution",
};
@@ -0,0 +1,3 @@
import ConwayControls from "./ConwayControls";
export { ConwayControls };
@@ -0,0 +1,5 @@
export type ConwayColorProp =
| "liveColor"
| "deadColor"
| "startingEnvelopeColor"
| "finalEnvelopeColor";
@@ -0,0 +1,72 @@
import { Grid, Typography, Button } from "@mui/material";
import {
defaultGameOptions,
GameContext,
RulesetName,
} from "../../../internal";
import { ConwayControls } from "./ConwayControls";
import { PokemonControls } from "./PokemonControls";
import { RockPaperScissorsControls } from "./RockPaperScissorsControls";
import { MazeControls } from "./MazeControls";
import { SnowflakeControls } from "./SnowflakeControls";
import { WarControls } from "./WarControls.ts";
import { WaterFlowControls } from "./WaterFlowControls";
import { useContext, useMemo } from "react";
import { ControlsContext } from "../context";
const GameSpecificControls = () => {
const { currentGame } = useContext(GameContext);
const { newGameSpecificConfigs, setNewGameSpecificConfigs } =
useContext(ControlsContext);
const Component = useMemo(() => {
switch (currentGame) {
case RulesetName.CONWAY:
return () => <ConwayControls />;
case RulesetName.MAZE_GENERATOR:
return () => <MazeControls />;
case RulesetName.POKEMON:
return () => <PokemonControls />;
case RulesetName.ROCK_PAPER_SCISSORS:
return () => <RockPaperScissorsControls />;
case RulesetName.SNOWFLAKE:
return () => <SnowflakeControls />;
case RulesetName.WAR:
return () => <WarControls />;
case RulesetName.WATER_FLOW:
return () => <WaterFlowControls />;
default:
return null;
}
}, [currentGame]);
return (
<>
{Component === null ? (
<Grid item xs={12}>
<Typography>
No settings have been developed for this game yet.
</Typography>
</Grid>
) : (
<Component />
)}
{Component !== null && (
<Grid item xs={12}>
<Button
onClick={() => {
setNewGameSpecificConfigs({
...newGameSpecificConfigs,
[currentGame]: defaultGameOptions[currentGame],
});
}}
>
Reset to Default
</Button>
</Grid>
)}
</>
);
};
export default GameSpecificControls;
@@ -0,0 +1,43 @@
import { Grid } from "@mui/material";
import { MuiColorInput } from "mui-color-input";
import { MazeConfig, RulesetName } from "../../../../internal";
import { MazeColorProp } from "./types";
import { useGameSpecificOptions } from "../hooks";
const mazeColorProps = ["liveColor", "deadColor"] as MazeColorProp[];
const MazeControls = () => {
const { config, handleUpdate } = useGameSpecificOptions<MazeConfig>(
RulesetName.MAZE_GENERATOR
);
return (
<>
<Grid container item xs={12} spacing={2}>
{mazeColorProps.map((color: MazeColorProp) => {
let label = "Wall Color";
if (color === "deadColor") {
label = "Background Color";
}
return (
<Grid item key={`maze-color-config-${color}`}>
<MuiColorInput
format="hex"
label={label}
value={config[color]}
onChange={(newColor) =>
handleUpdate({
...config,
[color]: newColor,
})
}
/>
</Grid>
);
})}
</Grid>
</>
);
};
export default MazeControls;
@@ -0,0 +1,3 @@
import MazeControls from "./MazeControls";
export { MazeControls };
@@ -0,0 +1 @@
export type MazeColorProp = "liveColor" | "deadColor";
@@ -0,0 +1,136 @@
import { useState } from "react";
import {
Grid,
TextField,
FormControlLabel,
Checkbox,
FormGroup,
FormControl,
FormLabel,
MenuItem,
} from "@mui/material";
import { MuiColorInput } from "mui-color-input";
import {
PokemonGameConfig,
PokemonType,
RulesetName,
} from "../../../../internal";
import { useGameSpecificOptions } from "../hooks";
const PokemonControls = () => {
const { config, handleUpdate } = useGameSpecificOptions<PokemonGameConfig>(
RulesetName.POKEMON
);
const [typeToChangeColor, setTypeToChangeColor] = useState<PokemonType>(
PokemonType.BUG
);
const { allowedTypes, typeColors } = config;
const allTypesAllowed =
allowedTypes.length === Object.keys(PokemonType).length;
return (
<>
<Grid item xs={12}>
<TextField
fullWidth
label="Mutation Odds (1 in X)"
type="number"
onChange={(e) =>
handleUpdate({
...config,
randomMutationChance: parseInt(e.target.value),
})
}
value={config.randomMutationChance}
/>
</Grid>
<Grid container item xs={12}>
<FormControl>
<FormLabel>Enable / Disable Pokemon Types</FormLabel>
<FormGroup row>
<FormControlLabel
control={
<Checkbox
checked={allTypesAllowed}
onChange={() => {
let newTypes = Object.keys(PokemonType).map(
(key) => PokemonType[key]
);
handleUpdate({
...config,
allowedTypes: allTypesAllowed ? [] : newTypes,
});
}}
name={`enable-type-select-all-checkbox`}
value={!allTypesAllowed}
/>
}
label={`${allTypesAllowed ? "Deselect" : "Select"} All`}
/>
{Object.keys(PokemonType).map((key) => {
const type = PokemonType[key];
return (
<FormControlLabel
control={
<Checkbox
checked={allowedTypes.includes(type)}
onChange={(e) => {
let newTypes = [...allowedTypes];
if (e.target.checked) {
newTypes.push(type);
} else {
newTypes = newTypes.filter(
(newType) => newType !== type
);
}
handleUpdate({
...config,
allowedTypes: newTypes,
});
}}
name={`enable-type-${type}-checkbox`}
value={allowedTypes.includes(type)}
/>
}
key={`pokemon-type-form-${key}`}
label={type.toString()}
/>
);
})}
</FormGroup>
</FormControl>
</Grid>
<Grid item md={6} xs={12}>
<TextField
fullWidth
label="Change Color of Type"
onChange={(e) => {
setTypeToChangeColor(e.target.value as PokemonType);
}}
select
value={typeToChangeColor}
>
{Object.keys(PokemonType).map((key) => (
<MenuItem key={key} value={PokemonType[key]}>
{PokemonType[key]}
</MenuItem>
))}
</TextField>
</Grid>
<Grid item md={6} xs={12}>
<MuiColorInput
format="hex"
label={`${typeToChangeColor} Color`}
value={typeColors[typeToChangeColor]}
onChange={(newColor) =>
handleUpdate({
...config,
typeColors: { ...typeColors, [typeToChangeColor]: newColor },
})
}
/>
</Grid>
</>
);
};
export default PokemonControls;
@@ -0,0 +1,3 @@
import PokemonControls from "./PokemonControls";
export { PokemonControls };
@@ -0,0 +1,34 @@
import { Grid } from "@mui/material";
import { MuiColorInput } from "mui-color-input";
import { RPSGameConfig, RulesetName } from "../../../../internal";
import { useGameSpecificOptions } from "../hooks";
const RockPaperScissorsControls = () => {
const { config, handleUpdate } = useGameSpecificOptions<RPSGameConfig>(
RulesetName.ROCK_PAPER_SCISSORS
);
return (
<>
{["rockColor", "paperColor", "scissorsColor"].map(
(color: keyof RPSGameConfig, i) => (
<Grid item xs={12} key={`rpg-color-config-${color}`}>
<MuiColorInput
format="hex"
label={`Color ${i + 1}`}
value={config[color]}
onChange={(newColor) =>
handleUpdate({
...config,
[color]: newColor,
})
}
/>
</Grid>
)
)}
</>
);
};
export default RockPaperScissorsControls;
@@ -0,0 +1,3 @@
import RockPaperScissorsControls from "./RockPaperScissorsControls";
export { RockPaperScissorsControls };
@@ -0,0 +1,43 @@
import { Grid } from "@mui/material";
import { MuiColorInput } from "mui-color-input";
import { SnowflakeGameConfig, RulesetName } from "../../../../internal";
import { SnowflakeColorProp } from "./types";
import { useGameSpecificOptions } from "../hooks";
const mazeColorProps = ["liveColor", "deadColor"] as SnowflakeColorProp[];
const SnowflakeControls = () => {
const { config, handleUpdate } = useGameSpecificOptions<SnowflakeGameConfig>(
RulesetName.SNOWFLAKE
);
return (
<>
<Grid container item xs={12} spacing={2}>
{mazeColorProps.map((color: SnowflakeColorProp) => {
let label = "Alive Color";
if (color === "deadColor") {
label = "Dead Color";
}
return (
<Grid item key={`snowflake-color-config-${color}`}>
<MuiColorInput
format="hex"
label={label}
value={config[color]}
onChange={(newColor) =>
handleUpdate({
...config,
[color]: newColor,
})
}
/>
</Grid>
);
})}
</Grid>
</>
);
};
export default SnowflakeControls;
@@ -0,0 +1,3 @@
import SnowflakeControls from "./SnowflakeControls";
export { SnowflakeControls };
@@ -0,0 +1 @@
export type SnowflakeColorProp = "liveColor" | "deadColor";
@@ -0,0 +1,68 @@
import { Grid, IconButton, Typography } from "@mui/material";
import CloseIcon from "@mui/icons-material/Close";
import AddIcon from "@mui/icons-material/Add";
import { MuiColorInput } from "mui-color-input";
import {
RulesetName,
WarGameConfig,
getRandomColor,
} from "../../../../internal";
import { useState } from "react";
import { useGameSpecificOptions } from "../hooks";
const WarControls = () => {
const { config, handleUpdate } = useGameSpecificOptions<WarGameConfig>(
RulesetName.WAR
);
const [newArmyColor, setNewArmyColor] = useState<string>(getRandomColor());
const { factionColors } = config;
return (
<>
<Grid container item xs={12} spacing={2}>
{factionColors.map((color, i) => {
return (
<Grid item key={`army-${color}`} alignItems="center">
<Typography sx={{ color, display: "inline" }}>
Army {i + 1}
</Typography>
<IconButton
onClick={() => {
const newFactions = factionColors.filter(
(existingColor) => existingColor !== color
);
handleUpdate({ factionColors: newFactions });
}}
>
<CloseIcon />
</IconButton>
</Grid>
);
})}
</Grid>
<Grid container item xs={12} alignItems="center">
<Grid item>
<MuiColorInput
format="hex"
label={"New Army Color"}
value={newArmyColor}
onChange={(newColor) => setNewArmyColor(newColor)}
/>
</Grid>
<Grid item>
<IconButton
onClick={() => {
const newFactions = [...factionColors, newArmyColor];
handleUpdate({ factionColors: newFactions });
setNewArmyColor(getRandomColor());
}}
>
<AddIcon />
</IconButton>
</Grid>
</Grid>
</>
);
};
export default WarControls;
@@ -0,0 +1,3 @@
import WarControls from "./WarControls";
export { WarControls };
@@ -0,0 +1,182 @@
import { useState } from "react";
import {
Checkbox,
FormControlLabel,
Grid,
IconButton,
TextField,
Typography,
} from "@mui/material";
import CloseIcon from "@mui/icons-material/Close";
import AddIcon from "@mui/icons-material/Add";
import { MuiColorInput } from "mui-color-input";
import {
RulesetName,
WaterFlowGameConfig,
getRandomColor,
} from "../../../../internal";
import { useGameSpecificOptions } from "../hooks";
const WaterFlowControls = () => {
const [newWaterColor, setNewWaterColor] = useState(getRandomColor());
const { config, handleUpdate } = useGameSpecificOptions<WaterFlowGameConfig>(
RulesetName.WATER_FLOW
);
const { waterColors } = config;
return (
<>
<Grid container item xs={12} spacing={2}>
<Grid item>
<TextField
fullWidth
label="Average Number of Starting Streams"
type="number"
onChange={(e) =>
handleUpdate({
...config,
averageStartingWaterCells: parseInt(e.target.value),
})
}
value={config.averageStartingWaterCells}
/>
</Grid>
<Grid item>
<TextField
fullWidth
label="Branching-Off Chance"
type="number"
onChange={(e) =>
handleUpdate({
...config,
branchingChance: parseInt(e.target.value),
})
}
value={config.branchingChance}
/>
</Grid>
<Grid item>
<TextField
fullWidth
label="Rock Health Variation"
type="number"
onChange={(e) =>
handleUpdate({
...config,
rockHealthVariance: parseInt(e.target.value),
})
}
value={config.rockHealthVariance}
/>
</Grid>
<Grid item>
<FormControlLabel
control={
<Checkbox
checked={config.trueRandom}
onChange={(e) => {
handleUpdate({
...config,
trueRandom: e.target.checked,
});
}}
name={"true-random-checkbox"}
value={config.trueRandom}
/>
}
label={"Random Flow Direction"}
/>
</Grid>
</Grid>
<Grid container item xs={12} spacing={2} alignItems="center">
<Grid item>
<FormControlLabel
control={
<Checkbox
checked={config.blurBackground}
onChange={(e) => {
handleUpdate({
...config,
blurBackground: e.target.checked,
});
}}
name={"blur-background-checkbox"}
/>
}
label={"Blur Background"}
/>
</Grid>
<Grid item>
{config.blurBackground && (
<TextField
fullWidth
label="Blur Amount"
type="number"
onChange={(e) =>
handleUpdate({
...config,
backgroundBlurAmount: parseInt(e.target.value),
})
}
value={config.backgroundBlurAmount}
/>
)}
</Grid>
</Grid>
<Grid container item xs={12} spacing={2}>
{waterColors.map((color, i) => {
return (
<Grid item key={`water-color-${color}`} alignItems="center">
<Typography sx={{ color, display: "inline" }}>
Color {i + 1}
</Typography>
<IconButton
onClick={() => {
const newWaterColors = waterColors.filter(
(existingColor) => existingColor !== color
);
handleUpdate({ ...config, waterColors: newWaterColors });
}}
>
<CloseIcon />
</IconButton>
</Grid>
);
})}
</Grid>
<Grid container item xs={12} spacing={2} alignItems="center">
<Grid item>
<MuiColorInput
format="hex"
label={"New Water Color"}
value={newWaterColor}
onChange={(newColor) => setNewWaterColor(newColor)}
/>
</Grid>
<Grid item>
<IconButton
onClick={() => {
const newWaterColors = [...waterColors, newWaterColor];
handleUpdate({ ...config, waterColors: newWaterColors });
setNewWaterColor(getRandomColor());
}}
>
<AddIcon />
</IconButton>
</Grid>
<Grid item>
<MuiColorInput
format="hex"
label={"Rock Color"}
value={config.baseBackgroundColor}
onChange={(newColor) =>
handleUpdate({ ...config, baseBackgroundColor: newColor })
}
/>
</Grid>
</Grid>
</>
);
};
export default WaterFlowControls;
@@ -0,0 +1,3 @@
import WaterFlowControls from "./WaterFlowControls";
export { WaterFlowControls };
@@ -0,0 +1 @@
export { default as useGameSpecificOptions } from "./useGameSpecificOptions";
@@ -0,0 +1,25 @@
import { useCallback, useContext, useMemo } from "react";
import { ControlsContext } from "../../context";
import { RulesetName } from "../../../../internal";
const useGameSpecificOptions = <T>(rulesetName: RulesetName) => {
const { newGameSpecificConfigs, setNewGameSpecificConfigs } =
useContext(ControlsContext);
const config = newGameSpecificConfigs[rulesetName] as T;
const handleUpdate = useCallback(
(newConfig: T) => {
setNewGameSpecificConfigs({
...newGameSpecificConfigs,
[rulesetName]: newConfig,
});
},
// eslint-disable-next-line react-hooks/exhaustive-deps
[newGameSpecificConfigs, setNewGameSpecificConfigs]
);
return useMemo(() => ({ config, handleUpdate }), [config, handleUpdate]);
};
export default useGameSpecificOptions;
@@ -0,0 +1,4 @@
import GameSpecificControls from "./GameSpecificControls";
export * from "./types";
export { GameSpecificControls };
@@ -0,0 +1,8 @@
import { GameSpecificConfig, RulesetName } from "../../../internal";
export type UpdateConfigFn = (
prop: RulesetName,
value: GameSpecificConfig
) => void;
export interface GameSpecificControlsProps {}
@@ -0,0 +1,145 @@
import {
Grid,
Typography,
Divider,
TextField,
MenuItem,
FormControl,
FormLabel,
RadioGroup,
FormControlLabel,
Radio,
Button,
Checkbox,
} from "@mui/material";
import {
CellShape,
defaultCellSize,
defaultGlobalConfigs,
defaultThrottleAmount,
GameContext,
GameDescription,
rulesetDisplayNameMap,
RulesetName,
} from "../../../internal";
import { GlobalControlsProps } from "./types";
import { useContext } from "react";
const GlobalControls = ({ config, setConfig }: GlobalControlsProps) => {
const { currentGame, setCurrentGame } = useContext(GameContext);
const { gridConfig, throttleAmount } = config;
const updateGridConfig = (prop: string, value: any) => {
setConfig({
...config,
gridConfig: { ...gridConfig, [prop]: value },
});
};
return (
<>
<Grid item xs={12}>
<Typography>Global Settings</Typography>
<Divider />
</Grid>
<Grid item xs={12}>
<TextField
fullWidth
helperText={<GameDescription rulesetName={currentGame} />}
label="Game Mode"
onChange={(e) => {
setCurrentGame(e.target.value as RulesetName);
}}
select
value={currentGame}
>
{Object.keys(RulesetName).map((name) => {
return (
<MenuItem key={name} value={name}>
{rulesetDisplayNameMap[name]}
</MenuItem>
);
})}
</TextField>
</Grid>
<Grid item md={6} xs={12}>
<FormControl>
<FormLabel>Cell Shape</FormLabel>
<RadioGroup
row
name="cell-shape-buttons-group"
onChange={(e) => {
updateGridConfig(
"cellShape",
(e.target as HTMLInputElement).value as CellShape
);
}}
value={gridConfig.cellShape}
>
<FormControlLabel
value="hex"
control={<Radio />}
label="Hexagonal"
/>
<FormControlLabel
value="square"
control={<Radio />}
label="Square"
/>
</RadioGroup>
</FormControl>
</Grid>
<Grid item md={6} xs={12}>
<TextField
fullWidth
label="Cell Size"
type="number"
onChange={(e) =>
updateGridConfig(
"cellSize",
parseInt(e.target.value) ?? defaultCellSize
)
}
value={gridConfig.cellSize}
/>
</Grid>
<Grid item md={6} xs={12}>
<FormControlLabel
control={
<Checkbox
checked={gridConfig.loops}
onChange={(e) => {
updateGridConfig("loops", e.target.checked);
}}
name={`enable-type-select-all-checkbox`}
value={gridConfig.loops}
/>
}
label={"Wrap Grid"}
/>
</Grid>
<Grid item md={6} xs={12}>
<TextField
fullWidth
helperText="WARNING: Low throttle amounts may create patterns of flashing lights."
label="Animation Throttle Amount"
type="number"
onChange={(e) =>
setConfig({
...config,
throttleAmount: parseInt(e.target.value) ?? defaultThrottleAmount,
})
}
value={throttleAmount}
/>
</Grid>
<Grid item xs={12}>
<Button onClick={() => setConfig(defaultGlobalConfigs[currentGame])}>
Reset to Default
</Button>
</Grid>
</>
);
};
export default GlobalControls;
@@ -0,0 +1,4 @@
import GlobalControls from "./GlobalControls";
export * from "./types";
export { GlobalControls };
@@ -0,0 +1,7 @@
import { Dispatch, SetStateAction } from "react";
import { GlobalConfig } from "../../../internal";
export interface GlobalControlsProps {
config: GlobalConfig;
setConfig: Dispatch<SetStateAction<GlobalConfig>>;
}
@@ -0,0 +1,3 @@
const ResetGameDefaultsButton = () => {};
export default ResetGameDefaultsButton;
@@ -0,0 +1,4 @@
import ResetGameDefaultsButton from "./ResetGameDefaultsButton";
export * from "./types";
export { ResetGameDefaultsButton };
@@ -0,0 +1 @@
export interface ResetGameDefaultsButtonProps {}
@@ -0,0 +1,3 @@
const ResetGlobalDefaultsButton = () => {};
export default ResetGlobalDefaultsButton;
@@ -0,0 +1,4 @@
import ResetGlobalDefaultsButton from "./ResetGlobalDefaultsButton";
export * from "./types";
export { ResetGlobalDefaultsButton };
@@ -0,0 +1 @@
export interface ResetGlobalDefaultsButtonProps {}
@@ -0,0 +1,6 @@
import { createContext } from "react";
import { ControlsContextType } from "./types";
const ControlsContext = createContext<ControlsContextType>(null);
export default ControlsContext;
@@ -0,0 +1,13 @@
import useControlsContext from "./useControlsContext";
import ControlsContext from "./ControlsContext";
const ControlsContextProvider = ({ children }) => {
const contextValue = useControlsContext();
return (
<ControlsContext.Provider value={contextValue}>
{children}
</ControlsContext.Provider>
);
};
export default ControlsContextProvider;
+6
View File
@@ -0,0 +1,6 @@
import ControlsContext from "./ControlsContext";
import ControlsContextProvider from "./ControlsContextProvider";
import useControlsContext from "./useControlsContext";
export * from "./types";
export { ControlsContext, ControlsContextProvider, useControlsContext };
+20
View File
@@ -0,0 +1,20 @@
import { Dispatch, SetStateAction } from "react";
import {
GameSpecificConfigs,
GlobalConfigs,
RulesetName,
} from "../../../internal";
export type GlobalConfigsTouched = {
[key in RulesetName]: boolean;
};
export interface ControlsContextType {
globalConfigsTouched: GlobalConfigsTouched;
newGameSpecificConfigs: GameSpecificConfigs;
newGlobalConfigs: GlobalConfigs;
setGlobalConfigsTouched: Dispatch<SetStateAction<GlobalConfigsTouched>>;
setNewGameSpecificConfigs: Dispatch<SetStateAction<GameSpecificConfigs>>;
setNewGlobalConfigs: Dispatch<SetStateAction<GlobalConfigs>>;
start: (closeDrawer: () => void) => void;
}
@@ -0,0 +1,86 @@
import { useCallback, useContext, useMemo, useState } from "react";
import { ControlsContextType, GlobalConfigsTouched } from "./types";
import {
GameSpecificConfigs,
GameContext,
GlobalConfigs,
RulesetName,
clearCanvas,
createGrid,
} from "../../../internal";
let initialGlobalConfigsTouched = {};
Object.keys(RulesetName).forEach(
(key) => (initialGlobalConfigsTouched[RulesetName[key]] = false)
);
const useControlsContext = (): ControlsContextType => {
const {
currentGame,
gameSpecificConfigs,
globalConfigs,
paused,
setGameSpecificConfigs,
setGlobalConfigs,
setGrid,
togglePause,
} = useContext(GameContext);
const [newGlobalConfigs, setNewGlobalConfigs] = useState<GlobalConfigs>({
...globalConfigs,
});
const [newGameSpecificConfigs, setNewGameSpecificConfigs] =
useState<GameSpecificConfigs>({
...gameSpecificConfigs,
});
const [globalConfigsTouched, setGlobalConfigsTouched] =
useState<GlobalConfigsTouched>();
const start = useCallback(
(closeDrawer: () => void) => {
setGlobalConfigs({ ...newGlobalConfigs });
setGameSpecificConfigs({ ...newGameSpecificConfigs });
clearCanvas();
setGrid(
createGrid(
currentGame,
newGlobalConfigs[currentGame],
newGameSpecificConfigs[currentGame]
)
);
if (paused) togglePause();
closeDrawer();
},
[
currentGame,
newGameSpecificConfigs,
newGlobalConfigs,
paused,
setGameSpecificConfigs,
setGlobalConfigs,
setGrid,
togglePause,
]
);
return useMemo(() => {
return {
globalConfigsTouched,
newGameSpecificConfigs,
newGlobalConfigs,
setGlobalConfigsTouched,
setNewGameSpecificConfigs,
setNewGlobalConfigs,
start,
};
}, [
globalConfigsTouched,
newGameSpecificConfigs,
newGlobalConfigs,
setGlobalConfigsTouched,
setNewGameSpecificConfigs,
setNewGlobalConfigs,
start,
]);
};
export default useControlsContext;
+3
View File
@@ -0,0 +1,3 @@
import Controls from "./Controls";
export { Controls };
+3
View File
@@ -0,0 +1,3 @@
export interface ControlsProps {
closeDrawer: () => void;
}
@@ -0,0 +1,61 @@
import { Link } from "@mui/material";
import { RulesetName } from "../../internal";
import { GameDescriptionProps } from "./types";
const GameDescription = ({ rulesetName }: GameDescriptionProps) => {
switch (rulesetName) {
case RulesetName.CONWAY:
return <>The iconic cellular automaton.</>;
case RulesetName.MAZE_GENERATOR:
return (
<>
An interesting pattern discovered due to a bug while recreating
Conway's GoL.
</>
);
case RulesetName.POKEMON:
return (
<>
The color of each cell represents one of the 18 Pokémon types. Each
turn, every cell checks to see if it would beat its neighbors, and
takes on the type that would have dealt it the most damage if not.
</>
);
case RulesetName.ROCK_PAPER_SCISSORS:
return (
<>
Inspired by{" "}
<Link href="https://www.youtube.com/watch?v=TvZI6Xc0J1Y">EFrans</Link>
. If a majority of a cell's neighbors beat it, it takes that color.
</>
);
case RulesetName.SNOWFLAKE:
return (
<>
An aesthetically pleasing pattern discovered while messing around with
the settings for Conway's Game of Life.
</>
);
case RulesetName.WAR:
return (
<>
Inspired by /u/AlexanderDudarev's{" "}
<Link href="https://www.reddit.com/r/cellular_automata/comments/1bmicq6/a_simple_cellular_automaton_that_simulates_war/">
post
</Link>{" "}
on Reddit. Each turn, every cell takes a random color from its
neighbors.
</>
);
case RulesetName.WATER_FLOW:
return (
<>
Based on the idea of water eroding and flowing through rock. Streams
can occasionally meet with other streams and blend their colors
together.
</>
);
}
};
export default GameDescription;
+4
View File
@@ -0,0 +1,4 @@
import GameDescription from "./GameDescription";
export * from "./types";
export { GameDescription };
+5
View File
@@ -0,0 +1,5 @@
import { RulesetName } from "../../internal";
export interface GameDescriptionProps {
rulesetName: RulesetName;
}
+73
View File
@@ -0,0 +1,73 @@
import { useContext, useState } from "react";
import { Box, Fab, Collapse, Drawer } from "@mui/material";
import DownIcon from "@mui/icons-material/KeyboardArrowDown";
import UpIcon from "@mui/icons-material/KeyboardArrowUp";
import PauseIcon from "@mui/icons-material/Pause";
import PlayArrowIcon from "@mui/icons-material/PlayArrow";
import RefreshIcon from "@mui/icons-material/Refresh";
import SettingsIcon from "@mui/icons-material/Settings";
import { Canvas, CANVAS_ID, Controls, GameContext } from "../../internal";
import { ControlsContextProvider } from "../Controls/context";
const GameScreen = () => {
const { grid, paused, togglePause } = useContext(GameContext);
const [menuOpen, setMenuOpen] = useState(false);
const [controlsOpen, setControlsOpen] = useState(false);
return (
<>
<Box
display="flex"
justifyContent="center"
alignItems="center"
minHeight="100vh"
pl={grid.config.cellSize / 6}
>
<Canvas canvasAttributes={{ id: CANVAS_ID }} />
<Fab
onClick={() => setMenuOpen(!menuOpen)}
sx={{ position: "absolute", bottom: 15, right: 15 }}
>
{menuOpen ? <DownIcon /> : <UpIcon />}
</Fab>
<Collapse in={menuOpen}>
<Fab
onClick={() => {
setControlsOpen(true);
setMenuOpen(false);
}}
sx={{ position: "absolute", bottom: 100, right: 15 }}
>
<SettingsIcon />
</Fab>
<Fab
onClick={() => {
togglePause();
}}
sx={{ position: "absolute", bottom: 185, right: 15 }}
>
{paused ? <PlayArrowIcon /> : <PauseIcon />}
</Fab>
<Fab
onClick={() => {
grid.reset();
}}
sx={{ position: "absolute", bottom: 270, right: 15 }}
>
<RefreshIcon />
</Fab>
</Collapse>
</Box>
<ControlsContextProvider>
<Drawer
anchor="bottom"
open={controlsOpen}
onClose={() => setControlsOpen(false)}
>
<Controls closeDrawer={() => setControlsOpen(false)} />
</Drawer>
</ControlsContextProvider>
</>
);
};
export default GameScreen;
+3
View File
@@ -0,0 +1,3 @@
import GameScreen from "./GameScreen";
export { GameScreen };
+4
View File
@@ -0,0 +1,4 @@
export * from "./Canvas";
export * from "./Controls";
export * from "./GameDescription";
export * from "./GameScreen";
+15
View File
@@ -0,0 +1,15 @@
import { CellShape, RulesetName } from "../../internal";
const defaultCellShape: CellShape = "square";
const defaultCellSize = 16;
const defaultGridLoops = true;
const defaultRulesetName: RulesetName = RulesetName.CONWAY;
const defaultThrottleAmount = 10;
export {
defaultCellShape,
defaultCellSize,
defaultGridLoops,
defaultRulesetName,
defaultThrottleAmount,
};
@@ -0,0 +1,71 @@
import {
RulesetName,
Preset,
PokemonType,
ConwayConfig,
MazeConfig,
PokemonGameConfig,
RPSGameConfig,
SnowflakeGameConfig,
WarGameConfig,
WaterFlowGameConfig,
GameSpecificConfigs,
} from "../../../internal";
import defaultPokemonColors from "./defaultPokemonColors";
const defaultGameOptions: GameSpecificConfigs = {
[RulesetName.CONWAY]: {
cellLifespan: 10,
deadColor: "#293462",
envelopeGradientSteps: 20,
finalEnvelopeColor: "#D800A6",
neighborsNeededToReproduce: [3],
neighborsNeededToSurvive: [2, 3],
liveColor: "#B59410",
liveStartPercent: 10,
mortalCells: true,
preset: Preset.DEFAULT,
showEnvelope: true,
showEnvelopeGradient: true,
startingEnvelopeColor: "#5E78E0",
} as ConwayConfig,
[RulesetName.MAZE_GENERATOR]: {
deadColor: "#640d14",
neighborsNeededToReproduce: [3],
neighborsNeededToSurvive: [2, 3],
liveColor: "#90be6d",
liveStartPercent: 10,
} as MazeConfig,
[RulesetName.POKEMON]: {
// disable dragon type by default because it (and dragon-steel-fairy tricolor spirals) tend to dominate the screen
allowedTypes: Object.keys(PokemonType)
.map((key) => PokemonType[key])
.filter((type) => type !== PokemonType.DRAGON),
randomMutationChance: 750,
typeColors: defaultPokemonColors,
} as PokemonGameConfig,
[RulesetName.ROCK_PAPER_SCISSORS]: {
rockColor: "#FF218C",
paperColor: "#FFD800",
scissorsColor: "#21B1FF",
} as RPSGameConfig,
[RulesetName.SNOWFLAKE]: {
deadColor: "#161b33",
liveColor: "#639fab",
} as SnowflakeGameConfig,
[RulesetName.WAR]: {
factionColors: ["#edae49", "#d1495b", "#00798c", "#30638e", "#003d5b"],
} as WarGameConfig,
[RulesetName.WATER_FLOW]: {
averageStartingWaterCells: 20,
backgroundBlurAmount: 1,
baseBackgroundColor: "#0b0027",
blurBackground: true,
branchingChance: 5,
rockHealthVariance: 5,
trueRandom: false,
waterColors: ["#3e8090", "#1a546a", "#F6FDC3", "#D74B76", "#FF8080"],
} as WaterFlowGameConfig,
};
export default defaultGameOptions;
@@ -0,0 +1,24 @@
import { PokemonType } from "../../../internal";
const defaultPokemonColors = {
[PokemonType.NORMAL]: "#A8A77A",
[PokemonType.FIRE]: "#EE8130",
[PokemonType.WATER]: "#6390F0",
[PokemonType.ELECTRIC]: "#F7D02C",
[PokemonType.GRASS]: "#7AC74C",
[PokemonType.ICE]: "#96D9D6",
[PokemonType.FIGHTING]: "#C22E28",
[PokemonType.POISON]: "#A33EA1",
[PokemonType.GROUND]: "#E2BF65",
[PokemonType.FLYING]: "#A98FF3",
[PokemonType.PSYCHIC]: "#F95587",
[PokemonType.BUG]: "#A6B91A",
[PokemonType.ROCK]: "#B6A136",
[PokemonType.GHOST]: "#735797",
[PokemonType.DRAGON]: "#6F35FC",
[PokemonType.DARK]: "#705746",
[PokemonType.STEEL]: "#B7B7CE",
[PokemonType.FAIRY]: "#D685AD",
};
export default defaultPokemonColors;
@@ -0,0 +1,4 @@
import defaultPokemonColors from "./defaultPokemonColors";
import defaultGameOptions from "./defaultGameOptions";
export { defaultGameOptions, defaultPokemonColors };
@@ -0,0 +1,10 @@
import defaultGridConfig from "./defaultGridConfig";
import { GlobalConfig } from "../../../internal";
import { defaultThrottleAmount } from "../defaultConfigValues";
const defaultGlobalConfig: GlobalConfig = {
gridConfig: defaultGridConfig,
throttleAmount: defaultThrottleAmount,
};
export default defaultGlobalConfig;
@@ -0,0 +1,20 @@
import { createRuleset, getCanvasSize, GridConfig } from "../../../internal";
import {
defaultCellShape,
defaultCellSize,
defaultGridLoops,
defaultRulesetName,
} from "../defaultConfigValues";
const { height, width } = getCanvasSize();
const defaultGridConfig: GridConfig = {
cellShape: defaultCellShape,
cellSize: defaultCellSize,
height,
loops: defaultGridLoops,
ruleset: createRuleset(defaultRulesetName),
width,
};
export default defaultGridConfig;
@@ -0,0 +1,4 @@
import { defaultGlobalConfigs } from "./useDefaultGlobalConfig";
import useDefaultGlobalConfig from "./useDefaultGlobalConfig";
export { defaultGlobalConfigs, useDefaultGlobalConfig };
@@ -0,0 +1,48 @@
import { useMemo } from "react";
import {
GlobalConfig,
GlobalConfigs,
GridConfig,
RulesetName,
} from "../../../internal";
import defaultGlobalConfig from "./defaultGlobalConfig";
import defaultGridConfig from "./defaultGridConfig";
type Override = Omit<Partial<GlobalConfig>, "gridConfig"> & {
gridConfig?: Partial<GridConfig>;
};
const overrideDefaultGlobalConfig = (config: Override) => {
const { gridConfig, ...otherConfigs } = config;
return {
...defaultGlobalConfig,
...otherConfigs,
gridConfig: { ...defaultGridConfig, ...(gridConfig ? gridConfig : {}) },
} as GlobalConfig;
};
export const defaultGlobalConfigs: GlobalConfigs = {
[RulesetName.CONWAY]: overrideDefaultGlobalConfig({}),
[RulesetName.MAZE_GENERATOR]: overrideDefaultGlobalConfig({}),
[RulesetName.POKEMON]: overrideDefaultGlobalConfig({
gridConfig: { cellShape: "hex", loops: false },
}),
[RulesetName.ROCK_PAPER_SCISSORS]: overrideDefaultGlobalConfig({
gridConfig: { cellShape: "hex" },
}),
[RulesetName.SNOWFLAKE]: overrideDefaultGlobalConfig({
gridConfig: { cellSize: 8 },
}),
[RulesetName.WAR]: overrideDefaultGlobalConfig({
gridConfig: { cellShape: "hex" },
}),
[RulesetName.WATER_FLOW]: overrideDefaultGlobalConfig({
gridConfig: { cellShape: "hex", cellSize: 12, loops: false },
}),
};
const useDefaultGlobalConfig = (rulesetName: RulesetName) => {
return useMemo(() => defaultGlobalConfigs[rulesetName], [rulesetName]);
};
export default useDefaultGlobalConfig;
+3
View File
@@ -0,0 +1,3 @@
export * from "./defaultConfigValues";
export * from "./gameSpecificOptions";
export * from "./globalOptions";
+3
View File
@@ -0,0 +1,3 @@
export * from "./defaults";
export * from "./math";
export * from "./strings";
+3
View File
@@ -0,0 +1,3 @@
const angleOfHexagonalSide = (2 * Math.PI) / 6;
export default angleOfHexagonalSide;
+3
View File
@@ -0,0 +1,3 @@
import angleOfHexagonalSide from "./angleOfHexagonalSide";
export { angleOfHexagonalSide };
+3
View File
@@ -0,0 +1,3 @@
const CANVAS_ID = "canvas";
export default CANVAS_ID;
+3
View File
@@ -0,0 +1,3 @@
import CANVAS_ID from "./canvasId";
export { CANVAS_ID };
+7
View File
@@ -0,0 +1,7 @@
import { createContext } from "react";
import defaultGameContextValue from "./const";
import { GameContextType } from "./types";
const GameContext = createContext<GameContextType>(defaultGameContextValue);
export default GameContext;
+11
View File
@@ -0,0 +1,11 @@
import useGameContext from "./useGameContext";
import GameContext from "./GameContext";
const GameContextProvider = ({ children }) => {
const contextValue = useGameContext();
return (
<GameContext.Provider value={contextValue}>{children}</GameContext.Provider>
);
};
export default GameContextProvider;
+21
View File
@@ -0,0 +1,21 @@
import { GameContextType } from "./types";
import {
defaultGameOptions,
defaultGlobalConfigs,
defaultRulesetName,
} from "../internal";
const defaultGameContextValue: GameContextType = {
currentGame: defaultRulesetName,
gameSpecificConfigs: defaultGameOptions,
globalConfigs: defaultGlobalConfigs,
grid: null,
paused: false,
setCurrentGame: () => null,
setGameSpecificConfigs: () => null,
setGlobalConfigs: () => null,
setGrid: () => null,
togglePause: () => null,
};
export default defaultGameContextValue;
+6
View File
@@ -0,0 +1,6 @@
import GameContext from "./GameContext";
import GameContextProvider from "./GameContextProvider";
import useGameContext from "./useGameContext";
export * from "./types";
export { GameContext, GameContextProvider, useGameContext };
+24
View File
@@ -0,0 +1,24 @@
import { Dispatch, SetStateAction } from "react";
import { GameSpecificConfig, Grid, GridConfig, RulesetName } from "../internal";
export type GameSpecificConfigs = { [key in RulesetName]: GameSpecificConfig };
export interface GlobalConfig {
gridConfig: GridConfig;
throttleAmount: number;
}
export type GlobalConfigs = { [key in RulesetName]: GlobalConfig };
export interface GameContextType {
currentGame: RulesetName;
gameSpecificConfigs: GameSpecificConfigs;
globalConfigs: GlobalConfigs;
grid: Grid;
paused: boolean;
setCurrentGame: Dispatch<SetStateAction<RulesetName>>;
setGameSpecificConfigs: Dispatch<SetStateAction<GameSpecificConfigs>>;
setGlobalConfigs: Dispatch<SetStateAction<GlobalConfigs>>;
setGrid: Dispatch<SetStateAction<Grid>>;
togglePause: () => void;
}
+61
View File
@@ -0,0 +1,61 @@
import { useCallback, useMemo, useState } from "react";
import { GameContextType, GameSpecificConfigs, GlobalConfigs } from "./types";
import defaultGameContextValue from "./const";
import {
createGrid,
defaultRulesetName,
Grid,
defaultGlobalConfigs,
defaultGameOptions,
RulesetName,
} from "../internal";
const useGameContext = (): GameContextType => {
const [currentGame, setCurrentGame] =
useState<RulesetName>(defaultRulesetName);
const [globalConfigs, setGlobalConfigs] =
useState<GlobalConfigs>(defaultGlobalConfigs);
const [gameSpecificConfigs, setGameSpecificConfigs] =
useState<GameSpecificConfigs>(defaultGameOptions);
const [grid, setGrid] = useState<Grid>(
createGrid(
defaultRulesetName,
defaultGlobalConfigs[defaultRulesetName],
gameSpecificConfigs[defaultRulesetName]
)
);
const [paused, setPaused] = useState(defaultGameContextValue.paused);
const togglePause = useCallback(() => {
setPaused(!paused);
}, [paused]);
return useMemo(
() => ({
currentGame,
gameSpecificConfigs,
globalConfigs,
grid,
paused,
setCurrentGame,
setGameSpecificConfigs,
setGlobalConfigs,
setGrid,
togglePause,
}),
[
currentGame,
gameSpecificConfigs,
globalConfigs,
grid,
paused,
setCurrentGame,
setGameSpecificConfigs,
setGlobalConfigs,
setGrid,
togglePause,
]
);
};
export default useGameContext;
+3
View File
@@ -0,0 +1,3 @@
import useCanvas from "./useCanvas";
export { useCanvas };
+27
View File
@@ -0,0 +1,27 @@
import { useRef, useEffect, useContext } from "react";
import { GameContext } from "../internal";
const useCanvas = (draw: (frameCount: number) => void) => {
const { paused } = useContext(GameContext);
const canvasRef = useRef<HTMLCanvasElement>(null);
useEffect(() => {
let frameCount = 0;
let animationFrameId: number;
const render = () => {
frameCount++;
draw(frameCount);
animationFrameId = window.requestAnimationFrame(render);
};
if (!paused) render();
return () => {
window.cancelAnimationFrame(animationFrameId);
};
}, [draw, paused]);
return canvasRef;
};
export default useCanvas;
+5
View File
@@ -0,0 +1,5 @@
body {
background-color: #181a1b;
margin: 0;
overflow: hidden;
}
+14
View File
@@ -0,0 +1,14 @@
import React from "react";
import ReactDOM from "react-dom/client";
import "./index.css";
import { App } from "./internal";
const root = ReactDOM.createRoot(
document.getElementById("root") as HTMLElement
);
root.render(
<React.StrictMode>
<App />
</React.StrictMode>
);
+74
View File
@@ -0,0 +1,74 @@
/*
manage export order to avoid circular dependencies
https://medium.com/visual-development/how-to-fix-nasty-circular-dependency-issues-once-and-for-all-in-javascript-typescript-a04c987cf0de
this feels hacky but it stops the app from crashing for now
*/
export * from "./const/math";
export * from "./const/strings";
export * from "./utils/arrayUtils";
export * from "./utils/colorUtils";
export * from "./rulesets/RockPaperScissors/types";
export * from "./rulesets/Snowflake/types";
export * from "./rulesets/War/types";
export * from "./rulesets/types";
export * from "./classes/Cell/types";
export * from "./classes/Grid/types";
export * from "./classes/Ruleset/types";
export * from "./const/defaults/defaultConfigValues";
// todo: remove this
export const possibleColors = [
"#430f6e",
"#472765",
"#566888",
"#506e86",
"#37566f",
"#FFC0CB",
];
export { default as Cell } from "./classes/Cell/Cell";
// depends on Cell
export * from "./rulesets/Conway/types";
export * from "./rulesets/MazeGenerator/types";
export * from "./rulesets/Pokemon/types";
export * from "./rulesets/WaterFlow/types";
// depends on Cell, angleOfHexagonalSide, and CANVAS_ID
export * from "./utils/canvasUtils";
// depends on Cell
export { default as createHexRows } from "./utils/gridUtils/createHexRows";
export { default as createSquareRows } from "./utils/gridUtils/createSquareRows";
export { default as getCellById } from "./utils/gridUtils/getCellById";
export { default as getNeighbors } from "./utils/gridUtils/getNeighbors";
export { default as getRandomCell } from "./utils/gridUtils/getRandomCell";
export { default as Ruleset } from "./classes/Ruleset/Ruleset";
export { default as Grid } from "./classes/Grid/Grid";
export * from "./context/types";
export { default as defaultPokemonColors } from "./const/defaults/gameSpecificOptions/defaultPokemonColors";
export { default as defaultGameOptions } from "./const/defaults/gameSpecificOptions/defaultGameOptions";
export { default as Conway } from "./rulesets/Conway/Conway";
export { default as MazeGenerator } from "./rulesets/MazeGenerator/MazeGenerator";
export { default as Pokemon } from "./rulesets/Pokemon/Pokemon";
export { default as RockPaperScissors } from "./rulesets/RockPaperScissors/RockPaperScissors";
export { default as Snowflake } from "./rulesets/Snowflake/Snowflake";
export { default as War } from "./rulesets/War/War";
export { default as WaterFlow } from "./rulesets/WaterFlow/WaterFlow";
export { default as createRuleset } from "./utils/rulesetUtils/createRuleset";
export { default as createGrid } from "./utils/gridUtils/createGrid";
export {
defaultGlobalConfigs,
default as useDefaultGlobalConfig,
} from "./const/defaults/globalOptions/useDefaultGlobalConfig";
export { default as GameContext } from "./context/GameContext";
export { default as useCanvas } from "./hooks/useCanvas";
export { default as GameContextProvider } from "./context/GameContextProvider";
export { default as Canvas } from "./components/Canvas/Canvas";
export { default as GameDescription } from "./components/GameDescription/GameDescription";
export { default as Controls } from "./components/Controls/Controls";
export { default as GameScreen } from "./components/GameScreen/GameScreen";
export { default as App } from "./App";
+1
View File
@@ -0,0 +1 @@
/// <reference types="react-scripts" />
+121
View File
@@ -0,0 +1,121 @@
import {
defaultGameOptions,
getGradientSteps,
Grid,
Ruleset,
RulesetName,
} from "../../internal";
import { ConwayCell, ConwayConfig } from "./types";
class Conway implements Ruleset {
constructor(config: Partial<ConwayConfig> = {}) {
this.config = {
...defaultGameOptions[RulesetName.CONWAY],
...config,
} as ConwayConfig;
const {
envelopeGradientSteps,
finalEnvelopeColor,
showEnvelope,
showEnvelopeGradient,
startingEnvelopeColor,
} = this.config;
if (!showEnvelope) {
this.envelopeGradientColors = [];
return;
}
if (!showEnvelopeGradient) {
this.envelopeGradientColors = [startingEnvelopeColor];
return;
}
const envelopeGradientColors = getGradientSteps(
startingEnvelopeColor,
finalEnvelopeColor,
envelopeGradientSteps
);
this.envelopeGradientColors = envelopeGradientColors;
}
config: ConwayConfig;
envelopeGradientColors: string[];
init(grid: Grid) {
switch (this.config.preset) {
default:
// start with a random distribution
grid.iterateCells((cell: ConwayCell) => {
const isAlive =
Math.floor(Math.random() * 100) < this.config.liveStartPercent;
cell.config = {
age: 0,
isAlive,
nextAlive: false,
timesAlive: isAlive ? 1 : 0,
};
cell.currentColor = isAlive
? this.config.liveColor
: this.config.deadColor;
});
break;
}
}
update(grid: Grid) {
const {
deadColor,
envelopeGradientSteps,
liveColor,
mortalCells,
neighborsNeededToReproduce,
neighborsNeededToSurvive,
showEnvelope,
} = this.config;
const { envelopeGradientColors } = this;
grid.iterateCells((cell: ConwayCell) => {
const { isAlive, timesAlive } = cell.config;
let nextColor = deadColor;
const handleDeadCell = () => {
cell.config.age = 0;
cell.config.nextAlive = false;
if (showEnvelope && timesAlive > 0) {
nextColor =
envelopeGradientColors[timesAlive] ??
envelopeGradientColors[envelopeGradientColors.length - 1];
}
};
if (cell.config.age > this.config.cellLifespan) {
handleDeadCell();
cell.setNextColor(nextColor);
return;
}
const neighbors = grid.getNeighbors(cell);
const numberOfLiveNeighbors = neighbors.filter(
(neighbor: ConwayCell) => neighbor.config.isAlive
).length;
let nextAliveState = false;
if (
(isAlive && neighborsNeededToSurvive.includes(numberOfLiveNeighbors)) ||
(!isAlive && neighborsNeededToReproduce.includes(numberOfLiveNeighbors))
) {
nextAliveState = true;
}
cell.config.nextAlive = nextAliveState;
if (nextAliveState) {
nextColor = liveColor;
if (timesAlive < envelopeGradientSteps) {
cell.config.timesAlive++;
}
if (mortalCells) cell.config.age++;
} else {
handleDeadCell();
}
cell.setNextColor(nextColor);
});
grid.iterateCells((cell: ConwayCell) => {
cell.config.isAlive = cell.config.nextAlive;
cell.setCurrentColor(cell.nextColor);
});
}
}
export default Conway;
+4
View File
@@ -0,0 +1,4 @@
import Conway from "./Conway";
export * from "./types";
export { Conway };
+32
View File
@@ -0,0 +1,32 @@
import { Cell } from "../../internal";
export enum Preset {
DEFAULT = "default",
}
export interface ConwayCellConfig {
age: number;
isAlive: boolean;
nextAlive: boolean;
timesAlive: number;
}
export type ConwayCell = Omit<Cell, "config"> & {
config: ConwayCellConfig;
};
export interface ConwayConfig {
cellLifespan: number;
deadColor: string;
envelopeGradientSteps: number;
finalEnvelopeColor: string;
neighborsNeededToReproduce: number[];
neighborsNeededToSurvive: number[];
liveColor: string;
liveStartPercent: number;
mortalCells: boolean;
preset: Preset;
showEnvelope: boolean;
showEnvelopeGradient: boolean;
startingEnvelopeColor: string;
}
@@ -0,0 +1,52 @@
import { defaultGameOptions, Grid, Ruleset, RulesetName } from "../../internal";
import { MazeCell, MazeConfig } from "./types";
class MazeGenerator implements Ruleset {
constructor(config: Partial<MazeConfig> = {}) {
this.config = {
...defaultGameOptions[RulesetName.MAZE_GENERATOR],
...config,
} as MazeConfig;
}
config: MazeConfig;
init(grid: Grid) {
// start with a random distribution
grid.iterateCells((cell: MazeCell) => {
const isAlive =
Math.floor(Math.random() * 100) < this.config.liveStartPercent;
cell.config = { isAlive };
cell.currentColor = isAlive
? this.config.liveColor
: this.config.deadColor;
});
}
update(grid: Grid) {
grid.iterateCells((cell: MazeCell) => {
const neighbors = grid.getNeighbors(cell);
let nextAliveState = false;
const isAlive = cell.config.isAlive;
const numberOfLiveNeighbors = neighbors.filter(
(neighbor: MazeCell) => neighbor.config.isAlive
).length;
if (
(isAlive &&
this.config.neighborsNeededToSurvive.includes(
numberOfLiveNeighbors
)) ||
(!isAlive &&
this.config.neighborsNeededToReproduce.includes(
numberOfLiveNeighbors
))
) {
nextAliveState = true;
}
cell.config.isAlive = nextAliveState;
cell.setNextColor(
nextAliveState ? this.config.liveColor : this.config.deadColor
);
});
grid.iterateCells((cell) => cell.setCurrentColor(cell.nextColor));
}
}
export default MazeGenerator;
+4
View File
@@ -0,0 +1,4 @@
import MazeGenerator from "./MazeGenerator";
export * from "./types";
export { MazeGenerator };
+17
View File
@@ -0,0 +1,17 @@
import { Cell } from "../../internal";
export interface MazeCellConfig {
isAlive: boolean;
}
export type MazeCell = Omit<Cell, "config"> & {
config: MazeCellConfig;
};
export interface MazeConfig {
deadColor: string;
neighborsNeededToReproduce: number[];
neighborsNeededToSurvive: number[];
liveColor: string;
liveStartPercent: number;
}
+89
View File
@@ -0,0 +1,89 @@
import {
defaultGameOptions,
GlobalConfig,
Grid,
randomFromArray,
Ruleset,
RulesetName,
} from "../../internal";
import { typeMatchup, matchupScoring } from "./const";
import {
MatchupKey,
PokemonCell,
PokemonGameConfig,
PokemonType,
} from "./types";
class Pokemon implements Ruleset {
constructor(config: Partial<PokemonGameConfig> = {}) {
this.config = {
...defaultGameOptions[RulesetName.POKEMON],
...config,
} as PokemonGameConfig;
}
config: PokemonGameConfig;
defaultGlobalSettings: GlobalConfig;
init(grid: Grid) {
grid.iterateCells((cell) => {
const { allowedTypes, typeColors } = this.config;
const type = randomFromArray(allowedTypes) || PokemonType.ELECTRIC;
cell.config = { currentType: type };
cell.currentColor = typeColors[type];
});
}
update(grid: Grid) {
grid.iterateCells((cell) => {
const { allowedTypes, randomMutationChance, typeColors } = this.config;
const neighbors = grid.getNeighbors(cell);
const neighborTypes = neighbors.map(
(neighbor: PokemonCell) => neighbor.config.currentType
);
const { currentType } = cell.config;
const matchupForCurrentType = typeMatchup[currentType];
let nextType = cell.config.currentType;
// add random mutations to prevent stable states
if (Math.floor(Math.random() * randomMutationChance) === 1) {
nextType = randomFromArray(allowedTypes) || PokemonType.ELECTRIC;
} else {
let averageDamageDealt = 0;
let averageDamageReceived = 0;
const damageByType: { [key: string]: number } = {};
neighborTypes.forEach((neighborType) => {
if (!damageByType.neighborType) damageByType[neighborType] = 0;
Object.keys(matchupForCurrentType).forEach((key: MatchupKey) => {
if (matchupForCurrentType[key].includes(neighborType)) {
const damageScore = matchupScoring[key];
damageByType[neighborType] += damageScore;
if (damageScore > 0) {
averageDamageDealt += damageScore;
} else {
averageDamageReceived += damageScore;
}
}
});
averageDamageDealt /= neighbors.length;
averageDamageReceived /= neighbors.length;
});
if (averageDamageDealt > averageDamageReceived) {
let highestHit = 0;
Object.keys(damageByType).forEach((key: PokemonType) => {
if (damageByType[key] > highestHit && key !== currentType) {
highestHit = damageByType[key];
nextType = key;
}
});
}
}
cell.config.nextType = nextType;
cell.setNextColor(typeColors[nextType]);
});
grid.iterateCells((cell) => {
cell.setCurrentColor(cell.nextColor);
cell.config.currentType = cell.config.nextType;
});
}
}
export default Pokemon;
+439
View File
@@ -0,0 +1,439 @@
import { MatchupKey, PokemonType } from "./types";
export const matchupScoring = {
[MatchupKey.NO_EFFECT_ATTACKING]: 0.1,
[MatchupKey.NO_EFFECT_DEFENDING]: -0.1,
[MatchupKey.NOT_VERY_EFFECTIVE_ATTACKING]: 1,
[MatchupKey.NOT_VERY_EFFECTIVE_DEFENDING]: -1,
[MatchupKey.SUPER_EFFECTIVE_ATTACKING]: 4,
[MatchupKey.SUPER_EFFECTIVE_DEFENDING]: -4,
};
export const typeMatchup: {
[key: string]: {
[MatchupKey.NO_EFFECT_ATTACKING]: PokemonType[];
[MatchupKey.NO_EFFECT_DEFENDING]: PokemonType[];
[MatchupKey.NOT_VERY_EFFECTIVE_ATTACKING]: PokemonType[];
[MatchupKey.NOT_VERY_EFFECTIVE_DEFENDING]: PokemonType[];
[MatchupKey.SUPER_EFFECTIVE_ATTACKING]: PokemonType[];
[MatchupKey.SUPER_EFFECTIVE_DEFENDING]: PokemonType[];
};
} = {
[PokemonType.NORMAL]: {
[MatchupKey.NO_EFFECT_ATTACKING]: [PokemonType.GHOST],
[MatchupKey.NO_EFFECT_DEFENDING]: [PokemonType.GHOST],
[MatchupKey.NOT_VERY_EFFECTIVE_ATTACKING]: [
PokemonType.ROCK,
PokemonType.STEEL,
],
[MatchupKey.NOT_VERY_EFFECTIVE_DEFENDING]: [],
[MatchupKey.SUPER_EFFECTIVE_ATTACKING]: [],
[MatchupKey.SUPER_EFFECTIVE_DEFENDING]: [PokemonType.FIGHTING],
},
[PokemonType.FIRE]: {
[MatchupKey.NO_EFFECT_ATTACKING]: [],
[MatchupKey.NO_EFFECT_DEFENDING]: [],
[MatchupKey.NOT_VERY_EFFECTIVE_ATTACKING]: [
PokemonType.FIRE,
PokemonType.WATER,
PokemonType.ROCK,
PokemonType.DRAGON,
],
[MatchupKey.NOT_VERY_EFFECTIVE_DEFENDING]: [
PokemonType.FIRE,
PokemonType.GRASS,
PokemonType.ICE,
PokemonType.BUG,
PokemonType.STEEL,
PokemonType.FAIRY,
],
[MatchupKey.SUPER_EFFECTIVE_ATTACKING]: [
PokemonType.GRASS,
PokemonType.ICE,
PokemonType.BUG,
PokemonType.STEEL,
],
[MatchupKey.SUPER_EFFECTIVE_DEFENDING]: [
PokemonType.WATER,
PokemonType.GROUND,
PokemonType.ROCK,
],
},
[PokemonType.WATER]: {
[MatchupKey.NO_EFFECT_ATTACKING]: [],
[MatchupKey.NO_EFFECT_DEFENDING]: [],
[MatchupKey.NOT_VERY_EFFECTIVE_ATTACKING]: [
PokemonType.WATER,
PokemonType.GRASS,
PokemonType.DRAGON,
],
[MatchupKey.NOT_VERY_EFFECTIVE_DEFENDING]: [
PokemonType.FIRE,
PokemonType.WATER,
PokemonType.ICE,
PokemonType.STEEL,
],
[MatchupKey.SUPER_EFFECTIVE_ATTACKING]: [
PokemonType.FIRE,
PokemonType.GROUND,
PokemonType.ROCK,
],
[MatchupKey.SUPER_EFFECTIVE_DEFENDING]: [
PokemonType.ELECTRIC,
PokemonType.GRASS,
],
},
[PokemonType.ELECTRIC]: {
[MatchupKey.NO_EFFECT_ATTACKING]: [PokemonType.GROUND],
[MatchupKey.NO_EFFECT_DEFENDING]: [],
[MatchupKey.NOT_VERY_EFFECTIVE_ATTACKING]: [
PokemonType.ELECTRIC,
PokemonType.GRASS,
PokemonType.DRAGON,
],
[MatchupKey.NOT_VERY_EFFECTIVE_DEFENDING]: [
PokemonType.ELECTRIC,
PokemonType.FLYING,
PokemonType.STEEL,
],
[MatchupKey.SUPER_EFFECTIVE_ATTACKING]: [
PokemonType.WATER,
PokemonType.FLYING,
],
[MatchupKey.SUPER_EFFECTIVE_DEFENDING]: [PokemonType.GROUND],
},
[PokemonType.GRASS]: {
[MatchupKey.NO_EFFECT_ATTACKING]: [PokemonType.GROUND],
[MatchupKey.NO_EFFECT_DEFENDING]: [],
[MatchupKey.NOT_VERY_EFFECTIVE_ATTACKING]: [
PokemonType.ELECTRIC,
PokemonType.GRASS,
PokemonType.DRAGON,
],
[MatchupKey.NOT_VERY_EFFECTIVE_DEFENDING]: [
PokemonType.ELECTRIC,
PokemonType.FLYING,
PokemonType.STEEL,
],
[MatchupKey.SUPER_EFFECTIVE_ATTACKING]: [
PokemonType.WATER,
PokemonType.FLYING,
],
[MatchupKey.SUPER_EFFECTIVE_DEFENDING]: [PokemonType.GROUND],
},
[PokemonType.ICE]: {
[MatchupKey.NO_EFFECT_ATTACKING]: [],
[MatchupKey.NO_EFFECT_DEFENDING]: [],
[MatchupKey.NOT_VERY_EFFECTIVE_ATTACKING]: [
PokemonType.FIRE,
PokemonType.WATER,
PokemonType.ICE,
PokemonType.STEEL,
],
[MatchupKey.NOT_VERY_EFFECTIVE_DEFENDING]: [PokemonType.ICE],
[MatchupKey.SUPER_EFFECTIVE_ATTACKING]: [
PokemonType.GRASS,
PokemonType.GROUND,
PokemonType.FLYING,
PokemonType.DRAGON,
],
[MatchupKey.SUPER_EFFECTIVE_DEFENDING]: [
PokemonType.FIRE,
PokemonType.FIGHTING,
PokemonType.ROCK,
PokemonType.STEEL,
],
},
[PokemonType.FIGHTING]: {
[MatchupKey.NO_EFFECT_ATTACKING]: [PokemonType.GHOST],
[MatchupKey.NO_EFFECT_DEFENDING]: [],
[MatchupKey.NOT_VERY_EFFECTIVE_ATTACKING]: [
PokemonType.POISON,
PokemonType.FLYING,
PokemonType.PSYCHIC,
PokemonType.BUG,
PokemonType.FAIRY,
],
[MatchupKey.NOT_VERY_EFFECTIVE_DEFENDING]: [
PokemonType.BUG,
PokemonType.ROCK,
PokemonType.DARK,
],
[MatchupKey.SUPER_EFFECTIVE_ATTACKING]: [
PokemonType.NORMAL,
PokemonType.ICE,
PokemonType.ROCK,
PokemonType.DARK,
PokemonType.STEEL,
],
[MatchupKey.SUPER_EFFECTIVE_DEFENDING]: [
PokemonType.FLYING,
PokemonType.PSYCHIC,
PokemonType.FAIRY,
],
},
[PokemonType.POISON]: {
[MatchupKey.NO_EFFECT_ATTACKING]: [PokemonType.STEEL],
[MatchupKey.NO_EFFECT_DEFENDING]: [],
[MatchupKey.NOT_VERY_EFFECTIVE_ATTACKING]: [
PokemonType.POISON,
PokemonType.GROUND,
PokemonType.ROCK,
PokemonType.GHOST,
],
[MatchupKey.NOT_VERY_EFFECTIVE_DEFENDING]: [
PokemonType.GRASS,
PokemonType.FIGHTING,
PokemonType.POISON,
PokemonType.BUG,
PokemonType.FAIRY,
],
[MatchupKey.SUPER_EFFECTIVE_ATTACKING]: [
PokemonType.GRASS,
PokemonType.FAIRY,
],
[MatchupKey.SUPER_EFFECTIVE_DEFENDING]: [
PokemonType.GROUND,
PokemonType.PSYCHIC,
],
},
[PokemonType.GROUND]: {
[MatchupKey.NO_EFFECT_ATTACKING]: [PokemonType.FLYING],
[MatchupKey.NO_EFFECT_DEFENDING]: [PokemonType.ELECTRIC],
[MatchupKey.NOT_VERY_EFFECTIVE_ATTACKING]: [
PokemonType.GRASS,
PokemonType.BUG,
],
[MatchupKey.NOT_VERY_EFFECTIVE_DEFENDING]: [
PokemonType.POISON,
PokemonType.ROCK,
],
[MatchupKey.SUPER_EFFECTIVE_ATTACKING]: [
PokemonType.FIRE,
PokemonType.ELECTRIC,
PokemonType.POISON,
PokemonType.ROCK,
PokemonType.STEEL,
],
[MatchupKey.SUPER_EFFECTIVE_DEFENDING]: [
PokemonType.WATER,
PokemonType.GRASS,
PokemonType.ICE,
],
},
[PokemonType.FLYING]: {
[MatchupKey.NO_EFFECT_ATTACKING]: [],
[MatchupKey.NO_EFFECT_DEFENDING]: [PokemonType.GROUND],
[MatchupKey.NOT_VERY_EFFECTIVE_ATTACKING]: [
PokemonType.ELECTRIC,
PokemonType.ROCK,
PokemonType.STEEL,
],
[MatchupKey.NOT_VERY_EFFECTIVE_DEFENDING]: [
PokemonType.GRASS,
PokemonType.FIGHTING,
PokemonType.BUG,
],
[MatchupKey.SUPER_EFFECTIVE_ATTACKING]: [
PokemonType.GRASS,
PokemonType.FIGHTING,
PokemonType.BUG,
],
[MatchupKey.SUPER_EFFECTIVE_DEFENDING]: [
PokemonType.ELECTRIC,
PokemonType.ICE,
PokemonType.ROCK,
],
},
[PokemonType.PSYCHIC]: {
[MatchupKey.NO_EFFECT_ATTACKING]: [PokemonType.DARK],
[MatchupKey.NO_EFFECT_DEFENDING]: [],
[MatchupKey.NOT_VERY_EFFECTIVE_ATTACKING]: [
PokemonType.PSYCHIC,
PokemonType.STEEL,
],
[MatchupKey.NOT_VERY_EFFECTIVE_DEFENDING]: [
PokemonType.FIGHTING,
PokemonType.PSYCHIC,
],
[MatchupKey.SUPER_EFFECTIVE_ATTACKING]: [
PokemonType.FIGHTING,
PokemonType.POISON,
],
[MatchupKey.SUPER_EFFECTIVE_DEFENDING]: [
PokemonType.BUG,
PokemonType.GHOST,
PokemonType.DARK,
],
},
[PokemonType.BUG]: {
[MatchupKey.NO_EFFECT_ATTACKING]: [],
[MatchupKey.NO_EFFECT_DEFENDING]: [],
[MatchupKey.NOT_VERY_EFFECTIVE_ATTACKING]: [
PokemonType.FIRE,
PokemonType.FIGHTING,
PokemonType.POISON,
PokemonType.FLYING,
PokemonType.GHOST,
PokemonType.STEEL,
PokemonType.FAIRY,
],
[MatchupKey.NOT_VERY_EFFECTIVE_DEFENDING]: [
PokemonType.GRASS,
PokemonType.FIGHTING,
PokemonType.GROUND,
],
[MatchupKey.SUPER_EFFECTIVE_ATTACKING]: [
PokemonType.GRASS,
PokemonType.PSYCHIC,
PokemonType.DARK,
],
[MatchupKey.SUPER_EFFECTIVE_DEFENDING]: [
PokemonType.FIRE,
PokemonType.FLYING,
PokemonType.ROCK,
],
},
[PokemonType.ROCK]: {
[MatchupKey.NO_EFFECT_ATTACKING]: [],
[MatchupKey.NO_EFFECT_DEFENDING]: [],
[MatchupKey.NOT_VERY_EFFECTIVE_ATTACKING]: [
PokemonType.FIGHTING,
PokemonType.GROUND,
PokemonType.STEEL,
],
[MatchupKey.NOT_VERY_EFFECTIVE_DEFENDING]: [
PokemonType.NORMAL,
PokemonType.FIRE,
PokemonType.POISON,
PokemonType.FLYING,
],
[MatchupKey.SUPER_EFFECTIVE_ATTACKING]: [
PokemonType.FIRE,
PokemonType.ICE,
PokemonType.FLYING,
PokemonType.BUG,
],
[MatchupKey.SUPER_EFFECTIVE_DEFENDING]: [
PokemonType.WATER,
PokemonType.GRASS,
PokemonType.FIGHTING,
PokemonType.GROUND,
PokemonType.STEEL,
],
},
[PokemonType.GHOST]: {
[MatchupKey.NO_EFFECT_ATTACKING]: [PokemonType.NORMAL],
[MatchupKey.NO_EFFECT_DEFENDING]: [
PokemonType.NORMAL,
PokemonType.FIGHTING,
],
[MatchupKey.NOT_VERY_EFFECTIVE_ATTACKING]: [PokemonType.DARK],
[MatchupKey.NOT_VERY_EFFECTIVE_DEFENDING]: [
PokemonType.POISON,
PokemonType.BUG,
],
[MatchupKey.SUPER_EFFECTIVE_ATTACKING]: [
PokemonType.PSYCHIC,
PokemonType.GHOST,
],
[MatchupKey.SUPER_EFFECTIVE_DEFENDING]: [
PokemonType.GHOST,
PokemonType.DARK,
],
},
[PokemonType.DRAGON]: {
[MatchupKey.NO_EFFECT_ATTACKING]: [PokemonType.FAIRY],
[MatchupKey.NO_EFFECT_DEFENDING]: [],
[MatchupKey.NOT_VERY_EFFECTIVE_ATTACKING]: [PokemonType.STEEL],
[MatchupKey.NOT_VERY_EFFECTIVE_DEFENDING]: [
PokemonType.FIRE,
PokemonType.WATER,
PokemonType.ELECTRIC,
PokemonType.GRASS,
],
[MatchupKey.SUPER_EFFECTIVE_ATTACKING]: [PokemonType.DRAGON],
[MatchupKey.SUPER_EFFECTIVE_DEFENDING]: [
PokemonType.ICE,
PokemonType.DRAGON,
PokemonType.FAIRY,
],
},
[PokemonType.DARK]: {
[MatchupKey.NO_EFFECT_ATTACKING]: [],
[MatchupKey.NO_EFFECT_DEFENDING]: [PokemonType.PSYCHIC],
[MatchupKey.NOT_VERY_EFFECTIVE_ATTACKING]: [
PokemonType.FIGHTING,
PokemonType.DARK,
PokemonType.FAIRY,
],
[MatchupKey.NOT_VERY_EFFECTIVE_DEFENDING]: [
PokemonType.GHOST,
PokemonType.DARK,
],
[MatchupKey.SUPER_EFFECTIVE_ATTACKING]: [
PokemonType.PSYCHIC,
PokemonType.GHOST,
],
[MatchupKey.SUPER_EFFECTIVE_DEFENDING]: [
PokemonType.FIGHTING,
PokemonType.BUG,
PokemonType.FAIRY,
],
},
[PokemonType.STEEL]: {
[MatchupKey.NO_EFFECT_ATTACKING]: [],
[MatchupKey.NO_EFFECT_DEFENDING]: [PokemonType.POISON],
[MatchupKey.NOT_VERY_EFFECTIVE_ATTACKING]: [
PokemonType.FIRE,
PokemonType.WATER,
PokemonType.ELECTRIC,
PokemonType.STEEL,
],
[MatchupKey.NOT_VERY_EFFECTIVE_DEFENDING]: [
PokemonType.NORMAL,
PokemonType.GRASS,
PokemonType.ICE,
PokemonType.FLYING,
PokemonType.PSYCHIC,
PokemonType.BUG,
PokemonType.ROCK,
PokemonType.DRAGON,
PokemonType.STEEL,
PokemonType.FAIRY,
],
[MatchupKey.SUPER_EFFECTIVE_ATTACKING]: [
PokemonType.ICE,
PokemonType.ROCK,
PokemonType.FAIRY,
],
[MatchupKey.SUPER_EFFECTIVE_DEFENDING]: [
PokemonType.FIRE,
PokemonType.FIGHTING,
PokemonType.GROUND,
],
},
[PokemonType.FAIRY]: {
[MatchupKey.NO_EFFECT_ATTACKING]: [],
[MatchupKey.NO_EFFECT_DEFENDING]: [PokemonType.DRAGON],
[MatchupKey.NOT_VERY_EFFECTIVE_ATTACKING]: [
PokemonType.FIRE,
PokemonType.POISON,
PokemonType.STEEL,
],
[MatchupKey.NOT_VERY_EFFECTIVE_DEFENDING]: [
PokemonType.FIGHTING,
PokemonType.BUG,
PokemonType.DARK,
],
[MatchupKey.SUPER_EFFECTIVE_ATTACKING]: [
PokemonType.FIGHTING,
PokemonType.DRAGON,
PokemonType.DARK,
],
[MatchupKey.SUPER_EFFECTIVE_DEFENDING]: [
PokemonType.POISON,
PokemonType.STEEL,
],
},
};
+4
View File
@@ -0,0 +1,4 @@
import Pokemon from "./Pokemon";
export * from "./types";
export { Pokemon };
+48
View File
@@ -0,0 +1,48 @@
import { Cell } from "../../internal";
export interface PokemonCellConfig {
currentType: PokemonType;
nextType: PokemonType;
}
export type PokemonCell = Omit<Cell, "config"> & {
config: PokemonCellConfig;
};
export enum PokemonType {
BUG = "Bug",
DARK = "Dark",
DRAGON = "Dragon",
ELECTRIC = "Electric",
FAIRY = "Fairy",
FIGHTING = "Fighting",
FIRE = "Fire",
FLYING = "Flying",
GHOST = "Ghost",
GRASS = "Grass",
GROUND = "Ground",
ICE = "Ice",
NORMAL = "Normal",
POISON = "Poison",
PSYCHIC = "Psychic",
ROCK = "Rock",
STEEL = "Steel",
WATER = "Water",
}
export interface PokemonGameConfig {
allowedTypes: PokemonType[];
/* the odds that a cell will turn into a random pokemon type rather than follow normal rules.
prevents stagnation or types going extinct */
randomMutationChance: number;
typeColors: { [key in PokemonType]: string };
}
export enum MatchupKey {
NO_EFFECT_ATTACKING = "noEffectAttacking",
NO_EFFECT_DEFENDING = "noEffectDefending",
NOT_VERY_EFFECTIVE_ATTACKING = "notVeryEffectiveAttacking",
NOT_VERY_EFFECTIVE_DEFENDING = "notVeryEffectiveDefending",
SUPER_EFFECTIVE_ATTACKING = "superEffectiveAttacking",
SUPER_EFFECTIVE_DEFENDING = "superEffectiveDefending",
}
@@ -0,0 +1,57 @@
import {
defaultGameOptions,
Grid,
randomFromArray,
Ruleset,
RulesetName,
} from "../../internal";
import { RPSGameConfig } from "./types";
class RockPaperScissors implements Ruleset {
constructor(config: Partial<RPSGameConfig> = {}) {
this.config = {
...defaultGameOptions[RulesetName.ROCK_PAPER_SCISSORS],
...config,
} as RPSGameConfig;
}
config: RPSGameConfig;
init(grid: Grid) {
grid.iterateCells(
(cell) =>
(cell.currentColor = randomFromArray([
this.config.rockColor,
this.config.paperColor,
this.config.scissorsColor,
]))
);
}
update(grid: Grid) {
grid.iterateCells((cell) => {
const neighbors = grid.getNeighbors(cell);
let strongColor = this.config.rockColor;
let weakColor = this.config.paperColor;
if (cell.currentColor === this.config.rockColor) {
strongColor = this.config.paperColor;
weakColor = this.config.scissorsColor;
} else if (cell.currentColor === this.config.paperColor) {
strongColor = this.config.scissorsColor;
weakColor = this.config.rockColor;
}
const winningNeighbors = neighbors.filter(
(neighbor) => neighbor.currentColor === strongColor
).length;
const losingNeighbors = neighbors.filter(
(neighbor) => neighbor.currentColor === weakColor
).length;
let nextColor = cell.currentColor;
if (winningNeighbors > losingNeighbors) {
nextColor = strongColor;
}
cell.setNextColor(nextColor);
});
grid.iterateCells((cell) => cell.setCurrentColor(cell.nextColor));
}
}
export default RockPaperScissors;
+4
View File
@@ -0,0 +1,4 @@
import RockPaperScissors from "./RockPaperScissors";
export * from "./types";
export { RockPaperScissors };
+5
View File
@@ -0,0 +1,5 @@
export interface RPSGameConfig {
rockColor: string;
paperColor: string;
scissorsColor: string;
}
+45
View File
@@ -0,0 +1,45 @@
import { defaultGameOptions, Grid, Ruleset, RulesetName } from "../../internal";
import { SnowflakeGameConfig } from "./types";
class Snowflake implements Ruleset {
constructor(config: Partial<SnowflakeGameConfig> = {}) {
this.config = {
...defaultGameOptions[RulesetName.SNOWFLAKE],
...config,
} as SnowflakeGameConfig;
}
config: SnowflakeGameConfig;
init(grid: Grid) {
const { deadColor, liveColor } = this.config;
const maxY = grid.rows[0].length - 1;
const maxX = grid.rows.length - 1;
const halfMaxX = Math.round(maxX / 2);
const halfMaxY = Math.round(maxY / 2);
grid.iterateCells(
(cell) =>
(cell.currentColor =
[halfMaxX, halfMaxX - 1].includes(cell.x) &&
[halfMaxY, halfMaxY - 1].includes(cell.y)
? liveColor
: deadColor)
);
}
update(grid: Grid) {
const { deadColor, liveColor } = this.config;
grid.iterateCells((cell) => {
const neighbors = grid.getNeighbors(cell);
const liveNeighbors = neighbors.filter(
(neighbor) => neighbor.currentColor === liveColor
).length;
let nextColor = deadColor;
const liveNeighborsAllowed = [2, 3];
if (liveNeighborsAllowed.includes(liveNeighbors)) {
nextColor = liveColor;
}
cell.setNextColor(nextColor);
});
grid.iterateCells((cell) => cell.setCurrentColor(cell.nextColor));
}
}
export default Snowflake;

Some files were not shown because too many files have changed in this diff Show More