Skip to content
Snippets Groups Projects
Commit c604b783 authored by Brady James Garvin's avatar Brady James Garvin
Browse files

Initial commit.

parents
No related branches found
No related tags found
No related merge requests found
Showing
with 696 additions and 0 deletions
# Disable line-ending conversions for this repository.
* -text
# dependencies
/node_modules
# testing
/coverage
# production
/build
# environments
.env.local
.env.development.local
.env.test.local
.env.production.local
# logs
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# misc
*~
.DS_Store
[submodule "stylelint-config"]
path = stylelint-config
url = git@git.unl.edu:soft-core/soft-260/stylelint-config.git
[submodule "eslint-config"]
path = eslint-config
url = git@git.unl.edu:soft-core/soft-260/eslint-config.git
LICENSE 0 → 100644
MIT License
Copyright (c) 2017 Stephanie Valentine, Brady J. Garvin
Copyright (c) 2018 Brady J. Garvin, Vũ Nguyen
Copyright (c) 2019–2021 Brady J. Garvin, Shruti Daggumati
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
Subproject commit 24df42fb655d234b83c93b0fb24d012e4d9ecb58
{
"folders": [
{
"path": "."
}
],
"settings": {
"files.eol": "LF",
"files.exclude": {
"**/node_modules": true
},
"files.trimFinalNewlines": true,
"files.trimTrailingWhitespace": true
}
}
# dependencies
/node_modules
# testing
/coverage
# production
/build
# environments
.env.local
.env.development.local
.env.test.local
.env.production.local
# logs
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# misc
*~
.DS_Store
This diff is collapsed.
{
"name": "@unlsoft/graph-search",
"version": "1.0.0",
"description": "Starter code for the lab on graph search.",
"private": true,
"license": "UNLICENSED",
"scripts": {
"lint:css": "stylelint \"**/*.css\" \"**/*.module.css\" \"!coverage/**\"",
"lint:js": "eslint --max-warnings 0 ./src",
"lint": "run-s --continue-on-error lint:**",
"test-once": "react-scripts test --watchAll=false --coverage",
"test": "react-scripts test --watchAll --coverage",
"start": "react-scripts start",
"build": "react-scripts build",
"eject": "react-scripts eject"
},
"homepage": ".",
"dependencies": {
"@reduxjs/toolkit": "^1.6.0",
"@testing-library/jest-dom": "^5.14.1",
"@testing-library/react": "^12.0.0",
"@testing-library/user-event": "^13.1.9",
"classnames": "^2.3.1",
"npm-run-all": "^4.1.5",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"react-redux": "^7.2.4",
"react-router-dom": "^5.2.0",
"react-scripts": "^4.0.3",
"workbox-background-sync": "^5.1.3",
"workbox-broadcast-update": "^5.1.3",
"workbox-cacheable-response": "^5.1.3",
"workbox-core": "^5.1.3",
"workbox-expiration": "^5.1.3",
"workbox-google-analytics": "^5.1.3",
"workbox-navigation-preload": "^5.1.3",
"workbox-precaching": "^5.1.3",
"workbox-range-requests": "^5.1.3",
"workbox-routing": "^5.1.3",
"workbox-strategies": "^5.1.3",
"workbox-streams": "^5.1.3"
},
"devDependencies": {
"@unlsoft/eslint-config": "file:../eslint-config",
"@unlsoft/stylelint-config": "file:../stylelint-config",
"eslint-plugin-jest-dom": "^3.9.0",
"stylelint": "^13.13.1"
},
"stylelint": {
"extends": "@unlsoft/stylelint-config"
},
"eslintConfig": {
"extends": [
"react-app",
"@unlsoft/eslint-config/react"
]
},
"jest": {
"clearMocks": true,
"collectCoverageFrom": [
"src/features/**/*.js"
],
"resetMocks": false,
"restoreMocks": false
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
}
}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<meta
name="description"
content="Starter code for the lab on graph search."
/>
<meta name="theme-color" content="rgba(208 0 0 / 100%)" />
<link rel="icon" href="%PUBLIC_URL%/logo.svg" />
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo.svg" />
<title>Graph Search Lab</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
</body>
</html>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 152 152">
<rect x="0" y="0" width="152" height="152" fill="rgba(0 0 0 / 100%)" />
<path d="M147,1H90V42h10V75.673L53.532,2.393,52.648,1H2V42H12v66H2v41H62V108H52V74.336l46.467,73.271L99.351,149H150V108H140V42h10V1Z" stroke-width="3" stroke="rgba(255 255 255 / 100%)" fill="rgba(208 0 0 / 100%)">
</path>
</svg>
{
"short_name": "Graph Search",
"name": "Graph Search Lab",
"description": "Starter code for the lab on graph search.",
"icons": [
{
"src": "logo.svg",
"type": "image/svg+xml",
"sizes": "192x192 512x512",
"purpose": "any maskable"
}
],
"start_url": ".",
"display": "standalone",
"orientation": "portrait",
"theme_color": "rgba(208 0 0 / 100%)",
"background_color": "rgba(255 255 255 / 100%)"
}
import { useRef } from 'react';
import { Route } from 'react-router-dom';
import { Maze } from './features/maze/maze.js';
import { solveByBFS, solveByDijkstras, solveByAStar } from './features/maze/solvers.js';
import { Solution } from './features/maze/solution.js';
const MAZE_SIZE = 51;
export function App() {
const maze = useRef();
if (maze.current === undefined) {
maze.current = new Maze(MAZE_SIZE);
}
return (
<>
<Route exact path={'/'}>
<h1>Maze Solvers</h1>
<Solution caption={'Breadth-First Search'} maze={maze.current} solver={solveByBFS} />
<Solution caption={'Dijkstra\'s Algorithm'} maze={maze.current} solver={solveByDijkstras} />
<Solution caption={'A*'} maze={maze.current} solver={solveByAStar} />
</Route>
</>
);
}
export class Queue {
constructor() {
this.head = undefined;
this.tail = undefined;
this._size = 0;
}
get size() {
return this._size;
}
insert(element) {
const oldTail = this.tail;
this.tail = {
element,
next: undefined,
};
if (oldTail === undefined) {
this.head = this.tail;
} else {
oldTail.next = this.tail;
}
++this._size;
}
remove() {
console.assert(this.head !== undefined, 'Cannot remove an element from an empty queue');
const element = this.head.element;
this.head = this.head.next;
if (this.head === undefined) {
this.tail = undefined;
}
--this._size;
return element;
}
}
export class PriorityQueue {
constructor() {
this._vertices = [];
}
get size() {
return this._vertices.length;
}
insert(element, measure) {
let index = this._vertices.length;
while (index > 0) {
const parentIndex = Math.floor((index - 1) / 2);
const parent = this._vertices[parentIndex];
if (parent.measure < measure) {
break;
}
this._vertices[index] = parent;
index = parentIndex;
}
this._vertices[index] = {
element,
measure,
};
}
remove() {
console.assert(this._vertices.length > 0, 'Cannot remove an element from an empty priority queue');
const result = this._vertices[0].element;
const vertex = this._vertices[this._vertices.length - 1];
for (let index = 0; ;) {
this._vertices[index] = vertex;
let swapIndex = index;
for (const candidateIndex of [2 * index + 1, 2 * index + 2]) {
if (candidateIndex < this._vertices.length - 1 &&
this._vertices[candidateIndex].measure < this._vertices[swapIndex].measure) {
swapIndex = candidateIndex;
}
}
if (swapIndex === index) {
this._vertices[index] = vertex;
this._vertices.pop();
return result;
}
this._vertices[index] = this._vertices[swapIndex];
index = swapIndex;
}
}
}
export const CellType = {
WALL: Symbol('CellType.WALL'),
PASSAGEWAY: Symbol('CellType.PASSAGEWAY'),
CONSIDERED: Symbol('CellType.CONSIDERED'),
SOLUTION: Symbol('CellType.SOLUTION'),
};
const ADJACENCIES = [
[1, 0],
[0, 1],
[-1, 0],
[0, -1],
];
function randomElement(list) {
return list[Math.floor(Math.random() * list.length)];
}
function sum(list) {
let result = 0;
for (const element of list) {
result += element;
}
return result;
}
class Cell {
constructor(x, y, type) {
this.x = x;
this.y = y;
this.type = type;
this.possibleNeighbors = [];
}
get neighbors() {
if (this.type === CellType.WALL) {
return [];
}
return this.possibleNeighbors.filter((possibleNeighbor) => possibleNeighbor.type !== CellType.WALL);
}
}
export class Maze {
constructor(size, initialize = true) {
const indices = [...Array(size).keys()];
this.cells = indices.map(
(x) => indices.map(
(y) => new Cell(x, y, CellType.WALL),
),
);
for (const x of indices) {
for (const y of indices) {
const cell = this.getCell(x, y);
for (const [dx, dy] of ADJACENCIES) {
const possibleNeighbor = this.getCell(x + dx, y + dy);
if (possibleNeighbor !== undefined) {
cell.possibleNeighbors.push(possibleNeighbor);
}
}
}
}
if (initialize) {
console.assert(size > 2, `A perfect maze must be large enough to have an interior; its size cannot be ${size}.`);
console.assert(size % 2 === 1, `The size of a perfect maze must always be odd; it cannot be ${size}.`);
this._growPerfectMaze(1, 1);
this.entrance = this.getCell(0, 1);
this.exit = this.getCell(size - 1, size - 2);
} else {
this.entrance = undefined;
this.exit = undefined;
}
}
copy() {
const indices = [...Array(this.size).keys()];
const result = new Maze(this.size, false);
for (const x of indices) {
for (const y of indices) {
result.getCell(x, y).type = this.getCell(x, y).type;
}
}
if (this.entrance !== undefined) {
result.entrance = result.getCell(this.entrance.x, this.entrance.y);
}
if (this.exit !== undefined) {
result.exit = result.getCell(this.exit.x, this.exit.y);
}
return result;
}
get size() {
return this.cells.length;
}
get entrance() {
return this._entrance;
}
set entrance(entrance) {
if (entrance !== undefined) {
entrance.type = CellType.PASSAGEWAY;
}
this._entrance = entrance;
}
get exit() {
return this._exit;
}
set exit(exit) {
if (exit !== undefined) {
exit.type = CellType.PASSAGEWAY;
}
this._exit = exit;
}
getCell(x, y) {
if (x >= 0 && x < this.size && y >= 0 && y < this.size) {
return this.cells[x][y];
}
return undefined;
}
countCellsByType(types) {
return sum(
this.cells.map(
(row) => sum(
row.map(
(cell) => types.has(cell.type) ? 1 : 0,
),
),
),
);
}
_getTwoWalls(x, y, dx, dy) {
const firstCell = this.getCell(x + dx, y + dy);
const secondCell = this.getCell(x + 2 * dx, y + 2 * dy);
if (firstCell !== undefined &&
firstCell.type === CellType.WALL &&
secondCell !== undefined &&
secondCell.type === CellType.WALL) {
return [[firstCell, secondCell]];
}
return [];
}
_growPerfectMaze(startX, startY) {
this.getCell(startX, startY).type = CellType.PASSAGEWAY;
const stack = [[startX, startY]];
while (stack.length > 0) {
const [x, y] = stack[stack.length - 1];
const twoWalls = randomElement(ADJACENCIES.map((adjacency) => this._getTwoWalls(x, y, ...adjacency)).flat());
if (twoWalls !== undefined) {
const [firstCell, secondCell] = twoWalls;
firstCell.type = CellType.PASSAGEWAY;
secondCell.type = CellType.PASSAGEWAY;
stack.push([secondCell.x, secondCell.y]);
} else {
stack.pop();
}
}
}
}
import PropTypes from 'prop-types';
import styles from './solution.module.css';
import { CellType, Maze } from './maze.js';
const CLASSES = new Map([
[CellType.WALL, styles.wall],
[CellType.PASSAGEWAY, styles.passageway],
[CellType.CONSIDERED, styles.considered],
[CellType.SOLUTION, styles.solution],
]);
const CONSIDERED = new Set([
CellType.CONSIDERED,
CellType.SOLUTION,
]);
const SOLUTION = new Set([
CellType.SOLUTION,
]);
function DiagramCell(props) {
const className = CLASSES.get(props.maze.getCell(props.x, props.y).type);
return (
<rect x={props.x} y={props.y} width={1} height={1} className={className} strokeWidth={0} />
);
}
DiagramCell.propTypes = {
maze: PropTypes.instanceOf(Maze).isRequired,
x: PropTypes.number.isRequired,
y: PropTypes.number.isRequired,
};
function Diagram(props) {
const size = props.maze.size;
const indices = [...Array(size).keys()];
const cells = indices.map(
(x) => indices.map(
(y) => <DiagramCell key={`${x},${y}`} maze={props.maze} x={x} y={y} />,
),
);
return (
<svg viewBox={`0 0 ${size} ${size}`} className={styles.diagram}>
{cells}
</svg>
);
}
Diagram.propTypes = {
maze: PropTypes.instanceOf(Maze).isRequired,
};
export function Solution(props) {
if (props.maze !== undefined) {
const maze = props.maze.copy();
props.solver(maze);
return (
<section className={styles.section}>
<h2>{props.caption}</h2>
<Diagram maze={maze} />
<p>Number of cells considered: {maze.countCellsByType(CONSIDERED)}</p>
<p>Number of cells in solution: {maze.countCellsByType(SOLUTION)}</p>
</section>
);
}
return (
<section className={styles.section}>
<h2>{props.caption}</h2>
<p>[No maze to solve.]</p>
</section>
);
}
Solution.propTypes = {
caption: PropTypes.string.isRequired,
maze: PropTypes.instanceOf(Maze),
solver: PropTypes.func.isRequired,
};
:root {
--wall-color: rgba(47 47 47 / 100%);
--passageway-color: rgba(239 239 239 / 100%);
--considered-color: rgba(207 95 207 / 100%);
--solution-color: rgba(207 0 0 / 100%);
}
.section {
margin-bottom: 3em;
}
.diagram {
display: block;
margin: 1em;
width: calc(100% - 2em);
}
.wall {
stroke: none;
fill: var(--wall-color);
}
.passageway {
stroke: none;
fill: var(--passageway-color);
}
.considered {
stroke: none;
fill: var(--considered-color);
}
.solution {
stroke: none;
fill: var(--solution-color);
}
import { Queue, PriorityQueue } from './collections.js';
import { CellType } from './maze.js';
class Edge {
constructor(from, to, distance = undefined) {
this.from = from;
this.to = to;
this.distance = distance;
}
}
export function solveByBFS(maze) {
// TODO: stub
}
export function solveByDijkstras(maze) {
// TODO: stub
}
function heuristic(maze, cell) {
return 0; // TODO: stub
}
export function solveByAStar(maze) {
// TODO: stub
}
:root {
/* Colors */
--letterbox-color: rgba(0 0 0 / 100%);
--app-background-color: rgba(239 239 239 / 100%);
--font-color: rgba(0 0 0 / 100%);
/* Sizes */
--minimum-app-size: 300px;
}
body {
margin: 0;
font-family: sans-serif;
text-align: center;
color: var(--font-color);
}
#root {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
background: var(--letterbox-color);
}
#portrait {
position: relative;
box-sizing: border-box;
margin: auto;
padding: 1em;
min-width: var(--minimum-app-size);
min-height: var(--minimum-app-size);
width: 100%;
height: 100%;
max-width: 62.5vh;
background: var(--app-background-color);
overflow-x: hidden;
overflow-y: scroll;
transform: scale(1);
}
import React from 'react';
import ReactDOM from 'react-dom';
import { HashRouter as Router } from 'react-router-dom';
import { App } from './app.js';
import './index.css';
import * as serviceWorkerRegistration from './serviceWorkerRegistration.js';
ReactDOM.render(
<React.StrictMode>
<Router>
<div id="portrait">
<App />
</div>
</Router>
</React.StrictMode>,
document.getElementById('root'),
);
// Learn more about service workers: https://cra.link/PWA
serviceWorkerRegistration.register();
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment