├── src ├── img │ ├── bg.png │ ├── hero.png │ ├── evil1.png │ ├── evil3.png │ ├── evil4.png │ ├── villain3.svg │ ├── villain1.svg │ ├── villain2.svg │ └── diamond.svg ├── Hero.ts ├── Diamond.ts ├── index.ts ├── Person.ts ├── Pathfinder │ ├── Path.ts │ ├── Node.ts │ ├── Graph.ts │ └── Pathfinder.ts ├── Villain.ts ├── Game.ts ├── Item.ts ├── Position.ts ├── styles.css ├── VillainsManager.ts └── Field.ts ├── .gitignore ├── tsconfig.json └── package.json /src/img/bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/add2/game1/main/src/img/bg.png -------------------------------------------------------------------------------- /src/img/hero.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/add2/game1/main/src/img/hero.png -------------------------------------------------------------------------------- /src/img/evil1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/add2/game1/main/src/img/evil1.png -------------------------------------------------------------------------------- /src/img/evil3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/add2/game1/main/src/img/evil3.png -------------------------------------------------------------------------------- /src/img/evil4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/add2/game1/main/src/img/evil4.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.idea 2 | /.tmp 3 | 4 | /dist 5 | /node_modules 6 | 7 | *.js 8 | /game1.iml 9 | -------------------------------------------------------------------------------- /src/Hero.ts: -------------------------------------------------------------------------------- 1 | import Person from "./Person"; 2 | import Position from "./Position" 3 | 4 | export default class Hero extends Person { 5 | constructor(pos: Position, step: number) { 6 | super(pos, step); 7 | this.element.classList.add('hero') 8 | } 9 | } -------------------------------------------------------------------------------- /src/Diamond.ts: -------------------------------------------------------------------------------- 1 | import Item from "./Item"; 2 | import Position from "./Position" 3 | 4 | export default class Diamond extends Item { 5 | constructor(pos: Position, step: number) { 6 | super(pos, step); 7 | this.element.classList.add('diamond') 8 | } 9 | } -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import Game from "./Game"; 2 | import "./styles.css" 3 | 4 | document.addEventListener("DOMContentLoaded", function() { 5 | try { 6 | new Game(5, 5, 4) 7 | } catch (err) { 8 | console.log(err) 9 | alert(err) 10 | } 11 | }) 12 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2015", 4 | "module": "commonjs", 5 | "rootDir": "./src/", 6 | "strict": true, 7 | "strictNullChecks": true, 8 | }, 9 | "exclude": [ 10 | "node_modules" 11 | ] 12 | } -------------------------------------------------------------------------------- /src/Person.ts: -------------------------------------------------------------------------------- 1 | import Item from "./Item" 2 | import Position from "./Position" 3 | 4 | export default class Person extends Item { 5 | constructor(pos: Position, step: number) { 6 | super(pos, step); 7 | } 8 | 9 | moveTo(pos: Position) { 10 | this.pos = pos 11 | this.commitPosition() 12 | } 13 | } -------------------------------------------------------------------------------- /src/Pathfinder/Path.ts: -------------------------------------------------------------------------------- 1 | import Node from "./Node"; 2 | import Position from "../Position"; 3 | 4 | export default class Path { 5 | path: Node[] = [] 6 | 7 | append(node: Node) { 8 | this.path.push(node) 9 | } 10 | 11 | firstStep(): Position | undefined { 12 | if (this.path.length < 2) { 13 | return undefined 14 | } 15 | return this.path[this.path.length - 2].pos 16 | } 17 | } -------------------------------------------------------------------------------- /src/Pathfinder/Node.ts: -------------------------------------------------------------------------------- 1 | import Position from "../Position"; 2 | 3 | export default class Node { 4 | pos: Position 5 | cost: number 6 | adjacent: Set 7 | previous: Node | undefined 8 | 9 | constructor(pos: Position) { 10 | this.pos = pos 11 | this.cost = Number.MAX_VALUE 12 | this.adjacent = new Set() 13 | } 14 | 15 | eq(n: Node) { 16 | return this.pos.eq(n.pos) 17 | } 18 | } -------------------------------------------------------------------------------- /src/Villain.ts: -------------------------------------------------------------------------------- 1 | import Person from "./Person"; 2 | import Position from "./Position" 3 | 4 | export enum VillainClass { 5 | Villain1 = "villain1", 6 | Villain2 = "villain2", 7 | Villain3 = "villain3", 8 | } 9 | 10 | export default class Villain extends Person { 11 | constructor(pos: Position, step: number, className: VillainClass) { 12 | super(pos, step); 13 | this.element.classList.add(className) 14 | } 15 | } -------------------------------------------------------------------------------- /src/Game.ts: -------------------------------------------------------------------------------- 1 | import Field from "./Field"; 2 | import Villain, { VillainClass } from "./Villain"; 3 | 4 | export default class Game { 5 | constructor(width: number, height: number, numberOrDiamonds: number) { 6 | const field = new Field(width, height) 7 | 8 | field.appendVillain( 9 | new Villain(field.randomEmptyPosition(), field.step, VillainClass.Villain1) 10 | ) 11 | 12 | for (let i = 0; i < numberOrDiamonds; i++) { 13 | field.appendRandomDiamond() 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/Item.ts: -------------------------------------------------------------------------------- 1 | import Position from "./Position" 2 | 3 | export default class Item { 4 | pos: Position 5 | step: number 6 | element: HTMLDivElement 7 | 8 | constructor(pos: Position, step: number) { 9 | this.pos = pos 10 | this.step = step 11 | this.element = document.createElement('div') 12 | this.element.classList.add('item') 13 | } 14 | 15 | commitPosition() { 16 | this.commitVerticalPosition() 17 | this.commitHorizontalPosition() 18 | } 19 | 20 | commitVerticalPosition() { 21 | this.element.style.top = (this.pos.y * this.step) + "px"; 22 | } 23 | 24 | commitHorizontalPosition() { 25 | this.element.style.left = (this.pos.x * this.step) + "px"; 26 | } 27 | } -------------------------------------------------------------------------------- /src/Position.ts: -------------------------------------------------------------------------------- 1 | export default class Position { 2 | x: number 3 | y: number 4 | 5 | constructor(x: number, y: number) { 6 | this.x = x 7 | this.y = y 8 | } 9 | 10 | up() { 11 | this.y-- 12 | } 13 | 14 | down() { 15 | this.y++ 16 | } 17 | 18 | left() { 19 | this.x-- 20 | } 21 | 22 | right() { 23 | this.x++ 24 | } 25 | 26 | copy(): Position { 27 | return new Position(this.x, this.y) 28 | } 29 | 30 | eq(p: Position) { 31 | return this.x === p.x && this.y === p.y 32 | } 33 | 34 | distance(p: Position) { 35 | return Math.sqrt(Math.pow(Math.abs(this.x - p.x), 2) + Math.pow(Math.abs(this.y - p.y), 2)); 36 | } 37 | 38 | toString() { 39 | return this.x + ":" + this.y 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "game1", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "dev": "webpack --mode=development", 8 | "build": "webpack --mode=production" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "git+ssh://git@bitbucket.org/add242/game1.git" 13 | }, 14 | "author": "", 15 | "license": "ISC", 16 | "bugs": { 17 | "url": "https://bitbucket.org/add242/game1/issues" 18 | }, 19 | "homepage": "https://bitbucket.org/add242/game1#readme", 20 | "devDependencies": { 21 | "css-loader": "^6.7.3", 22 | "html-webpack-plugin": "^5.5.0", 23 | "mini-css-extract-plugin": "^2.7.2", 24 | "style-loader": "^3.3.1", 25 | "webpack": "5.76.0", 26 | "webpack-cli": "^5.0.1" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/img/villain3.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /src/styles.css: -------------------------------------------------------------------------------- 1 | html, body { 2 | height: 100%; 3 | width: 100%; 4 | } 5 | body { 6 | background-color: #DDD; 7 | align-items: center; 8 | display: flex; 9 | justify-content: center; 10 | } 11 | 12 | .container { 13 | position: relative; 14 | margin: auto auto; 15 | } 16 | 17 | .field { 18 | position: relative; 19 | background-image: url("img/bg.png"); 20 | border-style: inset; 21 | } 22 | 23 | .scoreboard { 24 | position: relative; 25 | margin-top: 1em; 26 | border-style: outset; 27 | text-align: center; 28 | } 29 | 30 | .item { 31 | position: absolute; 32 | width: 20px; 33 | height: 20px; 34 | transition: all 250ms linear; 35 | } 36 | .item.hero { 37 | background: url("img/hero.png") no-repeat; 38 | z-index: 100; 39 | } 40 | .item.diamond { 41 | background: url("img/diamond.svg") no-repeat; 42 | background-size: 20px; 43 | z-index: 1; 44 | } 45 | .item.villain1 { 46 | background: url("img/villain1.svg") no-repeat; 47 | z-index: 101; 48 | } 49 | .item.villain2 { 50 | background: url("img/villain2.svg") no-repeat; 51 | z-index: 101; 52 | } 53 | .item.villain3 { 54 | background: url("img/villain3.svg") no-repeat; 55 | z-index: 101; 56 | } -------------------------------------------------------------------------------- /src/VillainsManager.ts: -------------------------------------------------------------------------------- 1 | import Field from "./Field" 2 | import Villain from "./Villain"; 3 | import Diamond from "./Diamond"; 4 | import Pathfinder from "./Pathfinder/Pathfinder"; 5 | 6 | export default class VillainsManager { 7 | field: Field 8 | pathfinder: Pathfinder 9 | 10 | constructor(field: Field) { 11 | this.field = field 12 | this.pathfinder = new Pathfinder(field) 13 | } 14 | 15 | nextPosition(v: Villain) { 16 | const d = this.closestDiamond(v) 17 | 18 | if (d === undefined) { 19 | return undefined 20 | } 21 | const path = this.pathfinder.path(v.pos, d.pos) 22 | 23 | return path.firstStep() 24 | } 25 | 26 | closestDiamond(v: Villain): Diamond | undefined { 27 | const distances = new Map() 28 | for (const d of this.field.diamonds) { 29 | distances.set(d, d.pos.distance(v.pos)) 30 | } 31 | 32 | let closest: Diamond | undefined = undefined 33 | 34 | distances.forEach((n, d) => { 35 | if (closest === undefined) { 36 | closest = d 37 | } 38 | const dd = distances.get(closest) 39 | if (dd !== undefined && n < dd) { 40 | closest = d 41 | } 42 | }); 43 | 44 | return closest 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/img/villain1.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /src/Pathfinder/Graph.ts: -------------------------------------------------------------------------------- 1 | import Position from "../Position"; 2 | import Node from "./Node"; 3 | import Field from "../Field"; 4 | 5 | export default class Graph { 6 | map: Map 7 | 8 | constructor(field: Field) { 9 | this.map = new Map() 10 | 11 | for (let x = 0; x < field.width; x++) { 12 | for (let y = 0; y < field.height; y++) { 13 | const p = new Position(x, y) 14 | const n = new Node(p) 15 | this.map.set(p.toString(), n) 16 | } 17 | } 18 | 19 | for (let x = 0; x < field.width; x++) { 20 | for (let y = 0; y < field.height; y++) { 21 | const node = this.node(new Position(x, y)) 22 | 23 | const adjacent = [ 24 | new Position(x - 1, y), 25 | new Position(x + 1, y), 26 | new Position(x, y - 1), 27 | new Position(x, y + 1), 28 | ] 29 | 30 | for (let i = 0; i < adjacent.length; i++) { 31 | if (field.isInsideField(adjacent[i])) { 32 | node.adjacent.add(this.node(adjacent[i])) 33 | } 34 | } 35 | } 36 | } 37 | } 38 | 39 | node(p: Position): Node { 40 | const node = this.map.get(p.toString()) 41 | if (undefined === node) { 42 | throw new Error('Node not found ' + p.toString()) 43 | } 44 | return node 45 | } 46 | 47 | clean() { 48 | for (let node of this.map.values()) { 49 | node.cost = Number.MAX_VALUE 50 | node.previous = undefined 51 | } 52 | } 53 | } -------------------------------------------------------------------------------- /src/img/villain2.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /src/Pathfinder/Pathfinder.ts: -------------------------------------------------------------------------------- 1 | import Field from "./../Field"; 2 | import Position from "./../Position"; 3 | import Node from "./Node"; 4 | import Graph from "./Graph"; 5 | import Path from "./Path"; 6 | import Hero from "../Hero"; 7 | 8 | export default class Pathfinder { 9 | graph: Graph 10 | hero: Hero 11 | 12 | constructor(field: Field) { 13 | this.graph = new Graph(field) 14 | this.hero = field.hero 15 | } 16 | 17 | path(start: Position, end: Position): Path { 18 | this.graph.clean() 19 | return this.findPath( 20 | this.graph.node(start), 21 | this.graph.node(end), 22 | ) 23 | } 24 | 25 | findPath(start: Node, end: Node): Path { 26 | const reachable = new Set(); 27 | const explored = new Set(); 28 | 29 | start.cost = 0 30 | reachable.add(start) 31 | 32 | while (reachable.size) { 33 | // Choose some node we know how to reach. 34 | const node = this.chooseNode(reachable, end) 35 | 36 | // If we just got to the goal node, build and return the path. 37 | if (node.eq(end)) { 38 | return this.buildPath(end) 39 | } 40 | 41 | // Don't repeat ourselves. 42 | reachable.delete(node) 43 | explored.add(node) 44 | 45 | // Where can we get from here? 46 | const newReachable = this.getAdjacentNodes(node, explored) 47 | for (let adjacent of newReachable) { 48 | if (!reachable.has(adjacent)) { 49 | reachable.add(adjacent) 50 | } 51 | 52 | // If this is a new path, or a shorter path than what we have, keep it. 53 | if (node.cost + 1 < adjacent.cost) { 54 | adjacent.previous = node 55 | adjacent.cost = node.cost + 1 56 | } 57 | } 58 | } 59 | 60 | // If we get here, no path was found 61 | return new Path() 62 | } 63 | 64 | chooseNode(reachable: Set, end: Node): Node { 65 | let min_cost = Number.MAX_VALUE 66 | let best_node: Node | null = null 67 | 68 | for (let node of reachable) { 69 | const cost = node.cost + this.estimateDistance(node, end) 70 | 71 | if (min_cost > cost) { 72 | min_cost = cost 73 | best_node = node 74 | } 75 | } 76 | 77 | if (null === best_node) { 78 | throw new Error("Reachable node not found") 79 | } 80 | 81 | return best_node 82 | } 83 | 84 | estimateDistance(a: Node, b: Node): number { 85 | return Math.sqrt( Math.pow(a.pos.x - b.pos.x, 2) + Math.pow(a.pos.y - b.pos.y, 2)) 86 | } 87 | 88 | buildPath(to: Node) { 89 | const path = new Path() 90 | path.append(to) 91 | while (to.previous) { 92 | path.append(to.previous) 93 | to = to.previous 94 | } 95 | return path 96 | } 97 | 98 | getAdjacentNodes(node: Node, explored: Set) { 99 | const copyAdjacentNodes = new Set(node.adjacent); 100 | copyAdjacentNodes.delete(this.graph.node(this.hero.pos)) 101 | for (const n of explored) { 102 | copyAdjacentNodes.delete(n) 103 | } 104 | return copyAdjacentNodes 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /src/img/diamond.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/Field.ts: -------------------------------------------------------------------------------- 1 | import Item from "./Item"; 2 | import Hero from "./Hero"; 3 | import Villain from "./Villain"; 4 | import Diamond from "./Diamond"; 5 | import Position from "./Position"; 6 | import VillainsManager from "./VillainsManager"; 7 | 8 | interface Score { 9 | hero: number 10 | villain: number 11 | } 12 | 13 | export default class Field { 14 | step = 20 // px 15 | width: number 16 | height: number 17 | 18 | container: HTMLDivElement 19 | domField: HTMLDivElement 20 | scoreboard: HTMLDivElement 21 | 22 | score: Score = { 23 | hero: 0, 24 | villain: 0, 25 | } 26 | 27 | hero: Hero 28 | villains: Set 29 | diamonds: Set 30 | 31 | villainsManager: VillainsManager 32 | 33 | constructor(width: number, height: number) { 34 | if (width < 1 || height < 1) { 35 | throw new Error('Incorrect WIDTH or HEIGHT') 36 | } 37 | 38 | this.width = width 39 | this.height = height 40 | 41 | this.villains = new Set() 42 | this.diamonds = new Set() 43 | 44 | this.container = this.newContainer() 45 | this.domField = this.newDomFiled() 46 | this.scoreboard = this.newScoreboard() 47 | this.updateScoreView() 48 | 49 | this.hero = new Hero(this.centerPosition(), this.step) 50 | this.appendHero(this.hero) 51 | 52 | this.villainsManager = new VillainsManager(this) 53 | 54 | document.onkeydown = (e) => this.onKeyPress(e) 55 | } 56 | 57 | newContainer() { 58 | const div = document.createElement("div") 59 | div.className = "container" 60 | document.body.appendChild(div) 61 | return div 62 | } 63 | 64 | newDomFiled() { 65 | const div = document.createElement("div") 66 | div.className = "field" 67 | div.style.width = (this.width * this.step) + "px" 68 | div.style.height = (this.height * this.step) + "px" 69 | this.container.appendChild(div) 70 | return div 71 | } 72 | 73 | newScoreboard() { 74 | const div = document.createElement("div") 75 | div.className = "scoreboard" 76 | this.container.appendChild(div) 77 | return div 78 | } 79 | 80 | heroBonus() { 81 | this.score.hero++ 82 | this.updateScoreView() 83 | } 84 | 85 | villainBonus() { 86 | this.score.villain++ 87 | this.updateScoreView() 88 | } 89 | 90 | updateScoreView() { 91 | this.scoreboard.innerText = this.score.hero + " / " + this.score.villain 92 | } 93 | 94 | appendHero(item: Item) { 95 | item.commitPosition() 96 | this.domField.appendChild(item.element) 97 | } 98 | 99 | appendItem(item: Item): boolean { 100 | if (!this.isEmptyCell(item.pos)) { 101 | return false 102 | } 103 | item.commitPosition() 104 | this.domField.appendChild(item.element) 105 | return true 106 | } 107 | 108 | appendVillain(villain: Villain): boolean { 109 | if (this.appendItem(villain)) { 110 | this.villains.add(villain) 111 | return true 112 | } 113 | return false 114 | } 115 | 116 | appendDiamond(diamond: Diamond): boolean { 117 | if (this.appendItem(diamond)) { 118 | this.diamonds.add(diamond) 119 | return true 120 | } 121 | return false 122 | } 123 | 124 | appendRandomDiamond() { 125 | this.appendDiamond( 126 | new Diamond(this.randomEmptyPosition(), this.step) 127 | ) 128 | } 129 | 130 | isEmptyCell(pos: Position): boolean { 131 | return this.isInsideField(pos) 132 | && !this.isHeroCell(pos) 133 | && !this.isVillainCell(pos) 134 | && !this.isDiamondCell(pos) 135 | } 136 | 137 | isInsideField(pos: Position): boolean { 138 | return pos.x >= 0 && pos.y >= 0 && pos.x < this.height && pos.y < this.height 139 | } 140 | 141 | isHeroCell(pos: Position): boolean { 142 | return pos.eq(this.hero.pos) 143 | } 144 | 145 | isVillainCell(pos: Position): boolean { 146 | for (let villain of this.villains) { 147 | if (pos.eq(villain.pos)) { 148 | return true 149 | } 150 | } 151 | return false 152 | } 153 | 154 | isDiamondCell(pos: Position): boolean { 155 | return null !== this.getDiamondFromCell(pos) 156 | } 157 | 158 | getDiamondFromCell(pos: Position): Diamond | null { 159 | for (let diamond of this.diamonds) { 160 | if (pos.eq(diamond.pos)) { 161 | return diamond 162 | } 163 | } 164 | return null 165 | } 166 | 167 | centerPosition(): Position { 168 | return new Position( 169 | Math.round((this.width-1)/2), 170 | Math.round((this.height-1)/2) 171 | ) 172 | } 173 | 174 | randomEmptyPosition(): Position { 175 | const pos = new Position(0, 0) 176 | do { 177 | pos.x = Math.floor(Math.random() * this.width) 178 | pos.y = Math.floor(Math.random() * this.height) 179 | } while (!this.isEmptyCell(pos)) 180 | return pos 181 | } 182 | 183 | 184 | removeDiamond(d: Diamond) { 185 | if (this.diamonds.has(d)) { 186 | d.element.remove() 187 | this.diamonds.delete(d) 188 | } 189 | } 190 | 191 | heroCellProcessing(p: Position) { 192 | if (!this.isInsideField(p) || this.isVillainCell(p)) { 193 | return 194 | } 195 | 196 | this.hero.moveTo(p) 197 | 198 | const d = this.getDiamondFromCell(p) 199 | if (null !== d) { 200 | this.removeDiamond(d) 201 | this.heroBonus() 202 | this.appendRandomDiamond() 203 | } 204 | } 205 | 206 | villainCellProcessing(villain: Villain) { 207 | const p = this.villainsManager.nextPosition(villain) 208 | if (undefined === p) { 209 | return 210 | } 211 | 212 | villain.moveTo(p) 213 | 214 | const d = this.getDiamondFromCell(p) 215 | if (null !== d) { 216 | this.removeDiamond(d) 217 | this.villainBonus() 218 | this.appendRandomDiamond() 219 | } 220 | } 221 | 222 | onKeyPress(e: KeyboardEvent) { 223 | if (e.key === "ArrowUp") { 224 | e.preventDefault() 225 | const p = this.hero.pos.copy() 226 | p.up() 227 | this.heroCellProcessing(p) 228 | } else if (e.key === "ArrowDown") { 229 | e.preventDefault() 230 | const p = this.hero.pos.copy() 231 | p.down() 232 | this.heroCellProcessing(p) 233 | } else if (e.key === "ArrowLeft") { 234 | e.preventDefault() 235 | const p = this.hero.pos.copy() 236 | p.left() 237 | this.heroCellProcessing(p) 238 | } else if (e.key === "ArrowRight") { 239 | e.preventDefault() 240 | const p = this.hero.pos.copy() 241 | p.right() 242 | this.heroCellProcessing(p) 243 | } 244 | 245 | for (const v of this.villains) { 246 | this.villainCellProcessing(v) 247 | } 248 | } 249 | } 250 | --------------------------------------------------------------------------------