Skip to content

Commit e6d16bb

Browse files
feat: create puzzle game with 3 levels
https://replit.com/@remarkablemark/Water-Pipe
1 parent 332affa commit e6d16bb

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

66 files changed

+298
-83
lines changed

src/constants/index.ts

+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
export * from './pipes'
2+
export * from './scenes'
3+
export * from './tags'
4+
export * from './tiles'

src/constants/pipes.ts

+36
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
export enum EmptyPipe {
2+
'═' = '═',
3+
'║' = '║',
4+
'╔' = '╔',
5+
'╗' = '╗',
6+
'╚' = '╚',
7+
'╝' = '╝',
8+
'╠' = '╠',
9+
'╣' = '╣',
10+
'╦' = '╦',
11+
'╩' = '╩',
12+
'╬' = '╬',
13+
}
14+
15+
export enum FilledPipe {
16+
'━' = '━',
17+
'┃' = '┃',
18+
'┏' = '┏',
19+
'┓' = '┓',
20+
'┗' = '┗',
21+
'┛' = '┛',
22+
'┣' = '┣',
23+
'┫' = '┫',
24+
'┳' = '┳',
25+
'┻' = '┻',
26+
'╋' = '╋',
27+
}
28+
29+
export enum DirectionPipe {
30+
'△' = '△',
31+
'▷' = '▷',
32+
'▽' = '▽',
33+
'◁' = '◁',
34+
}
35+
36+
export const pipes = [...Object.values(EmptyPipe), ...Object.values(FilledPipe)]
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
export enum Scene {
22
game = 'game',
3+
preload = 'preload',
34
}

src/constants/tags.ts

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export enum Tag {
2+
pipe = 'pipe',
3+
}

src/constants/tiles.ts

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export const TILE_SIZE = 234

src/events/cursors.ts

-22
This file was deleted.

src/events/index.ts

-1
This file was deleted.

src/gameobjects/enemy.ts

-5
This file was deleted.

src/gameobjects/index.ts

-2
This file was deleted.

src/gameobjects/player.ts

-5
This file was deleted.

src/helpers/index.ts

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export * from './pipes'
2+
export * from './solution'

src/helpers/pipes.ts

+59
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import type { GameObj } from 'kaboom'
2+
3+
import { EmptyPipe } from '../constants'
4+
5+
/**
6+
* Rotates pipe 90 degrees.
7+
*
8+
* @param pipe - Pipe sprite.
9+
*/
10+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
11+
export function rotatePipe(pipe: GameObj<any>) {
12+
switch (pipe.type) {
13+
case EmptyPipe['═']:
14+
pipe.type = EmptyPipe['║']
15+
break
16+
17+
case EmptyPipe['║']:
18+
pipe.type = EmptyPipe['═']
19+
break
20+
21+
case EmptyPipe['╔']:
22+
pipe.type = EmptyPipe['╗']
23+
break
24+
25+
case EmptyPipe['╗']:
26+
pipe.type = EmptyPipe['╝']
27+
break
28+
29+
case EmptyPipe['╚']:
30+
pipe.type = EmptyPipe['╔']
31+
break
32+
33+
case EmptyPipe['╝']:
34+
pipe.type = EmptyPipe['╚']
35+
break
36+
37+
case EmptyPipe['╠']:
38+
pipe.type = EmptyPipe['╦']
39+
break
40+
41+
case EmptyPipe['╣']:
42+
pipe.type = EmptyPipe['╩']
43+
break
44+
45+
case EmptyPipe['╦']:
46+
pipe.type = EmptyPipe['╣']
47+
break
48+
49+
case EmptyPipe['╩']:
50+
pipe.type = EmptyPipe['╠']
51+
break
52+
}
53+
54+
pipe.angle += 90
55+
56+
if (pipe.angle >= 360) {
57+
pipe.angle -= 360
58+
}
59+
}

src/helpers/solution.ts

+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import type { GameObj } from 'kaboom'
2+
3+
import { Tag } from '../constants'
4+
5+
/**
6+
* Checks if level has been solved.
7+
*
8+
* @returns - If all the pipes are in the correct position.
9+
*/
10+
export function checkSolution(level: GameObj<unknown>): boolean {
11+
return level.get(Tag.pipe).reduce((accumulator, pipe) => {
12+
return accumulator && pipe.type === pipe.solution
13+
}, true)
14+
}

src/index.ts

+3
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
11
import { start } from './scenes'
22

33
start()
4+
5+
// press F1
6+
// debug.inspect = true;

src/levels/index.ts

+42
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import { TILE_SIZE } from '../constants'
2+
import { levels } from './levels'
3+
import { getTiles } from './tiles'
4+
5+
/**
6+
* Gets level data.
7+
*
8+
* @param level - Level number.
9+
* @returns - Level data.
10+
*/
11+
export function getLevel(level: number) {
12+
const { map, scale } = levels[level]
13+
14+
const tileWidth = TILE_SIZE * scale
15+
const tileHeight = TILE_SIZE * scale
16+
17+
const width = tileWidth * map[0].length
18+
const height = tileHeight * map.length
19+
20+
const options = {
21+
tileWidth,
22+
tileHeight,
23+
tiles: getTiles(scale),
24+
25+
pos: vec2(
26+
center().x + tileWidth / 2 - width / 2,
27+
center().y + tileHeight / 2 - height / 2,
28+
),
29+
}
30+
31+
return [map, options] as const
32+
}
33+
34+
/**
35+
* Checks if level exists.
36+
*
37+
* @param level - Level number.
38+
* @returns - Whether level exists.
39+
*/
40+
export function hasLevel(level: number): boolean {
41+
return Boolean(levels[level])
42+
}

src/levels/levels.ts

+42
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
// https://wikipedia.org/wiki/Box-drawing_characters
2+
// ▲ ▼ ► ◄ ↑ ↓ → ← ⇦ ⇧ ⇨ ⇩
3+
// △ ▷ ▽ ◁ ⬤ ◉ ◎
4+
// ┓ ┗ ┏ ┛ ┣ ┫ ┳ ┻ ╋ ━ ┃
5+
// ╔ ╗ ╚ ╝ ╠ ╣ ╦ ╩ ╬ ═ ║
6+
7+
export const levels = [
8+
// 0
9+
{
10+
// prettier-ignore
11+
map: [
12+
'▷═╗ ',
13+
' ║ ',
14+
' ╚═◁',
15+
],
16+
scale: 1,
17+
},
18+
19+
// 1
20+
{
21+
// prettier-ignore
22+
map: [
23+
'╔═╦═◁',
24+
'╚═╬═╗',
25+
'▷═╩═╝',
26+
],
27+
scale: 1,
28+
},
29+
30+
// 2
31+
{
32+
// prettier-ignore
33+
map: [
34+
' ╔══╦══╗ ',
35+
' ║╔═╬═╗║ ',
36+
'▷╣╠═╬═╣╠◁',
37+
' ║╚═╬═╝║ ',
38+
' ╚══╩══╝ ',
39+
],
40+
scale: 0.8,
41+
},
42+
]

src/levels/tiles.ts

+53
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import { DirectionPipe, EmptyPipe, pipes, Tag } from '../constants'
2+
3+
/**
4+
* Gets level map tile object.
5+
*
6+
* @param tileScale - Tile scale.
7+
* @returns - Tile object.
8+
*/
9+
export function getTiles(tileScale: number) {
10+
const tileComps = [anchor('center'), scale(tileScale)]
11+
12+
const staticComps = [...tileComps, color(135, 206, 235)]
13+
14+
return pipes.reduce(
15+
(accumulator: Record<string, () => unknown[]>, value) => {
16+
accumulator[value] = () => [
17+
...tileComps,
18+
sprite(value),
19+
area(),
20+
rotate(0),
21+
{ type: value, solution: value },
22+
Tag.pipe,
23+
]
24+
25+
return accumulator
26+
},
27+
{
28+
[DirectionPipe['△']]: () => [
29+
...staticComps,
30+
sprite(EmptyPipe['║']),
31+
{ type: DirectionPipe['△'] },
32+
],
33+
34+
[DirectionPipe['▷']]: () => [
35+
...staticComps,
36+
sprite(EmptyPipe['═']),
37+
{ type: DirectionPipe['▷'] },
38+
],
39+
40+
[DirectionPipe['▽']]: () => [
41+
...staticComps,
42+
sprite(EmptyPipe['║']),
43+
{ type: DirectionPipe['▽'] },
44+
],
45+
46+
[DirectionPipe['◁']]: () => [
47+
...staticComps,
48+
sprite(EmptyPipe['═']),
49+
{ type: DirectionPipe['◁'] },
50+
],
51+
},
52+
)
53+
}

src/scenes/game.ts

+19-16
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,25 @@
1-
import { addCursorKeys } from '../events'
2-
import { addEnemy, addPlayer } from '../gameobjects'
1+
import { Scene, Tag } from '../constants'
2+
import { checkSolution, rotatePipe } from '../helpers'
3+
import { getLevel, hasLevel } from '../levels'
34

4-
scene('game', () => {
5-
const player = addPlayer()
6-
7-
player.onUpdate(() => {
8-
player.angle += 120 * dt()
9-
})
5+
scene(Scene.game, (levelNumber: number) => {
6+
if (!hasLevel(levelNumber)) {
7+
levelNumber = 0
8+
}
109

11-
addCursorKeys(player)
10+
const level = addLevel(...getLevel(levelNumber))
1211

13-
onClick(() => addKaboom(mousePos()))
12+
level.get(Tag.pipe).forEach((pipe) => {
13+
Array(randi(4))
14+
.fill(undefined)
15+
.forEach(() => rotatePipe(pipe))
16+
})
1417

15-
add([text('Press arrow keys', { width: width() / 2 }), pos(12, 12)])
18+
onClick(Tag.pipe, (pipe) => {
19+
rotatePipe(pipe)
1620

17-
for (let i = 0; i < 3; i++) {
18-
const x = rand(0, width())
19-
const y = rand(0, height())
20-
addEnemy(x, y)
21-
}
21+
if (checkSolution(level)) {
22+
go(Scene.game, levelNumber + 1)
23+
}
24+
})
2225
})

src/scenes/index.ts

+8-2
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,8 @@
1-
export * from './game'
2-
export * from './start'
1+
import './game'
2+
import './preload'
3+
4+
import { Scene } from '../constants'
5+
6+
export function start() {
7+
go(Scene.preload)
8+
}

src/scenes/preload.ts

+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import { pipes, Scene } from '../constants'
2+
3+
scene(Scene.preload, async () => {
4+
await Promise.all(
5+
pipes.map((value) => {
6+
loadSprite(value, `sprites/pipes/${value}.png`)
7+
}),
8+
)
9+
10+
go(Scene.game, new URLSearchParams(location.search).get('level') || 0)
11+
})

src/scenes/start.ts

-5
This file was deleted.

src/sounds/score.mp3

-1.45 KB
Binary file not shown.

src/sprites/apple.png

-512 Bytes
Binary file not shown.

src/sprites/bag.png

-630 Bytes
Binary file not shown.

src/sprites/bean.png

-708 Bytes
Binary file not shown.

src/sprites/bean.ts

-6
This file was deleted.

src/sprites/bobo.png

-606 Bytes
Binary file not shown.

src/sprites/boom.png

-1.74 KB
Binary file not shown.

src/sprites/btfly.png

-647 Bytes
Binary file not shown.

src/sprites/cloud.png

-426 Bytes
Binary file not shown.

src/sprites/coin.png

-309 Bytes
Binary file not shown.

src/sprites/cursor_default.png

-263 Bytes
Binary file not shown.

src/sprites/cursor_pointer.png

-266 Bytes
Binary file not shown.

src/sprites/dino.png

-965 Bytes
Binary file not shown.

src/sprites/door.png

-569 Bytes
Binary file not shown.

src/sprites/egg.png

-430 Bytes
Binary file not shown.

src/sprites/egg_crack.png

-491 Bytes
Binary file not shown.

src/sprites/ghosty.png

-586 Bytes
Binary file not shown.

0 commit comments

Comments
 (0)