├── README.md
├── utils.js
├── world.js
├── index.html
└── index.js
/README.md:
--------------------------------------------------------------------------------
1 | # Start
2 | > you need node (npm) installed
3 |
4 | Run the following command in project root folder:
5 | ```
6 | npx http-server
7 | ```
8 |
9 | To change layout or controls visit configuration on the top left.
10 |
11 | Change control: Press a button, then the new key.
12 |
--------------------------------------------------------------------------------
/utils.js:
--------------------------------------------------------------------------------
1 | import rxjs from 'https://dev.jspm.io/rxjs@6';
2 | import operators from 'https://dev.jspm.io/rxjs@6/operators';
3 |
4 | const { fromEvent } = rxjs;
5 | const { startWith, map, tap, switchMap, shareReplay, take } = operators;
6 |
7 | const list = (length, fn) => Array.from({ length }, (_, i) => fn(i));
8 |
9 | const capNumber = (min, max, number) => Math.max(min, Math.min(max, number));
10 |
11 | const getInputNumberStream = element => fromEvent(element, 'input').pipe(
12 | map(({ target }) => target.value),
13 | startWith(element.value),
14 | map(val => Number(val)),
15 | );
16 |
17 | const getButtonClickStream = element => fromEvent(element, 'click').pipe(
18 | tap(event => event.preventDefault() && event.stopPropagation()),
19 | );
20 |
21 | const selectButtonFromKeyEventStream = (element, stream) => getButtonClickStream(element).pipe(
22 | switchMap(_ => stream.pipe(take(1))),
23 | map(({ key }) => key),
24 | tap(key => element.textContent = key),
25 | startWith(element.textContent),
26 | shareReplay(1),
27 | );
28 |
29 | const renderAsText = (observable, element) => observable.subscribe(value => element.textContent = String(value));
30 |
31 | export {
32 | list,
33 | capNumber,
34 | getInputNumberStream,
35 | getButtonClickStream,
36 | selectButtonFromKeyEventStream,
37 | renderAsText
38 | }
--------------------------------------------------------------------------------
/world.js:
--------------------------------------------------------------------------------
1 | import { list, capNumber } from './utils.js';
2 |
3 | const GROUND = 0;
4 | const PLAYER = 1;
5 | const BOULDER = 2;
6 |
7 | const generateWorld = (dimX, dimY) => list(dimY, () => list(dimX, i => Math.random() < 0.1 ? BOULDER : GROUND ));
8 |
9 | const updateWorld = (world, { event, payload }) => {
10 | switch (event) {
11 | case 'MovePlayer':
12 | const { player, worldMap } = world;
13 | const [ deltaX, deltaY ] = payload;
14 |
15 | // reset player render
16 | worldMap[player.y][player.x] = 0;
17 |
18 | // cap with world boundaries
19 | player.x = capNumber(0, worldMap[0].length - 1, player.x + deltaX);
20 | player.y = capNumber(0, worldMap.length - 1, player.y + deltaY);
21 |
22 | // set player render
23 | worldMap[player.y][player.x] = 1;
24 |
25 | return { worldMap, player };
26 | default:
27 | return world;
28 | }
29 | };
30 |
31 | const renderWorld = ([[{worldMap}, length], { x, y }, ctx]) => {
32 | // clear canvas
33 | ctx.clearRect(0,0, window.innerWidth, window.innerHeight);
34 |
35 | // draw world
36 | worldMap.forEach((worldSlice, i) => {
37 | const yPos = y + length * i;
38 | // console.log('yPos:', yPos);
39 | worldSlice.forEach((field, j) => {
40 | const xPos = x + length * j;
41 | // console.log('xPos:', xPos);
42 | switch (field) {
43 | case GROUND:
44 | ctx.fillStyle = 'green';
45 | break;
46 | case PLAYER:
47 | ctx.fillStyle = 'red';
48 | break;
49 | case BOULDER:
50 | ctx.fillStyle = 'brown';
51 | break;
52 | default:
53 | ctx.fillStyle = 'black';
54 | }
55 | ctx.fillRect(xPos, yPos, length, length);
56 | })
57 | });
58 | };
59 |
60 | export {
61 | generateWorld,
62 | updateWorld,
63 | renderWorld
64 | };
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
28 |
29 |
30 |
31 | Configuration
32 |
96 |
97 |
98 |
99 |
--------------------------------------------------------------------------------
/index.js:
--------------------------------------------------------------------------------
1 | import rxjs from 'https://dev.jspm.io/rxjs@6';
2 | import operators from 'https://dev.jspm.io/rxjs@6/operators';
3 |
4 | import { generateWorld, updateWorld, renderWorld } from './world.js';
5 | import { getInputNumberStream, selectButtonFromKeyEventStream, renderAsText } from './utils.js';
6 |
7 | const { fromEvent, BehaviorSubject, combineLatest, merge, NEVER } = rxjs;
8 | const { tap, share, startWith, scan, switchMap, withLatestFrom, map, shareReplay } = operators;
9 |
10 | // sources
11 | const canvasContext$ = new BehaviorSubject(canvas.getContext('2d'));
12 |
13 | const startX$ = getInputNumberStream(startX);
14 |
15 | const startY$ = getInputNumberStream(startY);
16 |
17 | const dimX$ = getInputNumberStream(dimX).pipe(
18 | tap(val => {
19 | startX.max = String(val - 1);
20 | if (Number(startX.value) > Number(startX.max)) {
21 | startX.value = startX.max;
22 | startX.dispatchEvent(new Event('input'));
23 | }
24 | })
25 | );
26 |
27 | const dimY$ = getInputNumberStream(dimY).pipe(
28 | tap(val => {
29 | startY.max = String(val - 1);
30 | if (Number(startY.value) > Number(startY.max)) {
31 | startY.value = startY.max;
32 | startY.dispatchEvent(new Event('input'));
33 | }
34 | })
35 | );
36 |
37 | const resize$ = fromEvent(window, 'resize').pipe(
38 | startWith(undefined),
39 | shareReplay(1)
40 | );
41 |
42 | const keydown$ = fromEvent(document, 'keydown').pipe(
43 | share()
44 | );
45 |
46 | const configurationOpen$ = fromEvent(configuration, 'toggle').pipe(
47 | map(({ target }) => target.open),
48 | startWith(configuration.open),
49 | shareReplay(1)
50 | );
51 |
52 | // intermediates
53 | const controlKey$ = configurationOpen$.pipe(
54 | switchMap(open => open ? NEVER : keydown$)
55 | );
56 |
57 | const configurationKey$ = configurationOpen$.pipe(
58 | switchMap(open => open ? keydown$ : NEVER)
59 | );
60 |
61 | const moveUpKey$ = selectButtonFromKeyEventStream(moveUp, configurationKey$);
62 | const moveDownKey$ = selectButtonFromKeyEventStream(moveDown, configurationKey$);
63 | const moveLeftKey$ = selectButtonFromKeyEventStream(moveLeft, configurationKey$);
64 | const moveRightKey$ = selectButtonFromKeyEventStream(moveRight, configurationKey$);
65 |
66 | const dimension$ = combineLatest(
67 | dimX$,
68 | dimY$
69 | ).pipe(
70 | map(([x, y]) => ({ x, y })),
71 | shareReplay(1)
72 | );
73 |
74 | const playerStartPosition$ = combineLatest(
75 | startX$,
76 | startY$
77 | ).pipe(
78 | map(([x, y]) => ({ x, y })),
79 | shareReplay(1)
80 | );
81 |
82 | const canvasCenter$ = resize$.pipe(
83 | tap(_ => (canvas.width = window.innerWidth) && (canvas.height = window.innerHeight)),
84 | map(_ => ({ x: window.innerWidth / 2, y: window.innerHeight / 2 })),
85 | );
86 |
87 | const canvasFieldLength$ = combineLatest(
88 | resize$,
89 | dimension$
90 | ).pipe(
91 | map(([_, { x, y }]) => Math.floor(Math.min(window.innerWidth / x, window.innerHeight / y))),
92 | shareReplay(1)
93 | );
94 |
95 | const canvasWorldStart$ = canvasFieldLength$.pipe(
96 | withLatestFrom(dimension$, canvasCenter$),
97 | map(([length, { x: dimX, y: dimY }, { x, y }]) => ({
98 | x: Math.round(x - length * dimX / 2),
99 | y: Math.round(y - length * dimY / 2)
100 | }))
101 | );
102 |
103 | const initWorld$ = combineLatest(
104 | dimension$,
105 | playerStartPosition$
106 | ).pipe(
107 | map(([{ x: dimX, y: dimY }, { x, y }]) => {
108 |
109 | const worldMap = generateWorld(dimX, dimY);
110 | worldMap[y][x] = 1;
111 | return { worldMap, player: { x, y } };
112 | })
113 | );
114 |
115 | const playerPositionDelta$ = controlKey$.pipe(
116 | withLatestFrom(
117 | moveUpKey$,
118 | moveDownKey$,
119 | moveLeftKey$,
120 | moveRightKey$
121 | ),
122 | map(([{ key }, moveUp, moveDown, moveLeft, moveRight]) => {
123 | switch (key) {
124 | case moveUp:
125 | return { event: 'MovePlayer', payload: [0, -1]};
126 | case moveDown:
127 | return { event: 'MovePlayer', payload: [0, +1]};
128 | case moveLeft:
129 | return { event: 'MovePlayer', payload: [-1, 0]};
130 | case moveRight:
131 | return { event: 'MovePlayer', payload: [+1, 0]};
132 | default:
133 | return { event: 'MovePlayer', payload: [0, 0] };
134 | }
135 | }),
136 | );
137 |
138 | const updateWorld$ = initWorld$.pipe(
139 | switchMap(initialWorld => merge(
140 | playerPositionDelta$
141 | ).pipe(
142 | scan(updateWorld, initialWorld),
143 | startWith(initialWorld)
144 | )),
145 | );
146 |
147 | const render$ = combineLatest(
148 | updateWorld$,
149 | canvasFieldLength$
150 | ).pipe(
151 | withLatestFrom(
152 | canvasWorldStart$,
153 | canvasContext$
154 | ),
155 | tap(renderWorld)
156 | );
157 |
158 | const keyName$ = keydown$.pipe(
159 | map(({ key }) => key)
160 | );
161 |
162 | // sinks
163 | renderAsText(keyName$, lastPressed);
164 | render$.subscribe();
165 |
--------------------------------------------------------------------------------