Created
April 10, 2018 10:56
-
-
Save stevekrouse/85f350eb82ebbf5b2c244aa9aeffc453 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import {run} from '@cycle/run'; | |
import {makeCanvasDriver, rect, text} from 'cycle-canvas'; | |
import { makeKeyboardDriver } from 'cycle-keyboard' | |
import xs from 'xstream' | |
import fromEvent from 'xstream/extra/fromEvent' | |
import split from 'xstream/extra/split' | |
import throttle from 'xstream/extra/throttle' | |
import concat from 'xstream/extra/concat' | |
import _ from 'lodash' | |
// useful links | |
// https://github.com/cyclejs-community/cycle-canvas/blob/master/examples/flappy-bird/app.js | |
// https://github.com/staltz/xstream/blob/master/EXTRA_DOCS.md#sampleCombine | |
// https://github.com/substack/box-collide | |
// https://github.com/staltz/xstream#of | |
// | |
// TODO | |
// make the bird freeze | |
// make the pipes freeze | |
// think about less brain taxing ways of encoding this, like the initial state, update way | |
// consider https://github.com/staltz/cycle-onionify | |
// restart game button | |
// pipe collisions | |
// think about encoding seconds, pixels and p/s and p/s^2 into types | |
const CANVAS_WIDTH = window.innerWidth | |
const CANVAS_HEIGHT = window.innerHeight | |
const BIRD_WIDTH = 100 | |
const BIRD_HEIGHT = 100 | |
function main ({KeyPress$, MouseDown$}) { | |
// BIRD | |
const TIME_DELTA = 10 // TODO this needs to be in more constants | |
const GRAVITY = 0.01 / TIME_DELTA | |
const JUMP_SPEED = 0.5 | |
const TIME_FROM_JUMP_TO_TOP_OF_ARC = JUMP_SPEED / GRAVITY | |
const JUMP_TIME = 2 * TIME_FROM_JUMP_TO_TOP_OF_ARC | |
const NEXT_JUMP_TIME = JUMP_TIME * 0.5 | |
const STARTING_Y_POSITION = 100 | |
const time$ = xs.periodic(TIME_DELTA) | |
const spaceKey$ = KeyPress$.filter(key => key.keyCode == 32) | |
const click$ = MouseDown$ | |
const jump$ = xs.merge(spaceKey$, click$).compose(throttle(NEXT_JUMP_TIME)) | |
const time$$ = time$.compose(split(jump$)) | |
const time$ToSpeed = (stream, start) => | |
stream.fold((speed, _) => speed + (GRAVITY * TIME_DELTA), start) | |
const firstYSpeed$$ = time$$.take(1).map(time$ => time$ToSpeed(time$, 0)) | |
// I really thought that it would make sense to have time$$.drop(1).map(...) here | |
// However, dropping 1 causes the bird to pause midair on the first space press | |
const restYSpeeds$$ = time$$.map(time$ => time$ToSpeed(time$, -JUMP_SPEED)) | |
const ySpeed$ = concat(firstYSpeed$$, restYSpeeds$$).flatten() | |
const yPosition$ = ySpeed$.fold((position, speed) => | |
position + (speed * TIME_DELTA), STARTING_Y_POSITION) | |
const outofBounds$ = yPosition$.map(position => position < 0 || position + BIRD_HEIGHT > CANVAS_HEIGHT) | |
// PIPES | |
const NEW_PIPE_TIME = 4000 | |
const makePipe = timeAdded => { | |
return { | |
x: CANVAS_WIDTH - 100, | |
yOffset: (Math.random() * 200) - 100, | |
timeAdded: timeAdded | |
} | |
} | |
// 0----------------1--2--3------------------4----------------------------- | |
const newPipeTime$ = xs.periodic(NEW_PIPE_TIME) | |
// [{x, yOffset}]----------------------------[{x, yOffset}, {x, yOffset}]-- | |
const staticPipe$ = newPipeTime$.fold((pipes, time) => | |
pipes.concat([makePipe(time + 1)]), [makePipe(0)]) | |
const pipes$ = xs.combine(time$, staticPipe$) | |
.map(([time, pipes]) => pipes.map(pipe => { | |
return { | |
x: pipe.x + ((pipe.timeAdded * NEW_PIPE_TIME / TIME_DELTA) - time), | |
yOffset: pipe.yOffset, | |
timeAdded: pipe.timeAdded | |
}}) | |
) | |
const PIPE_GAP = 300 | |
const PIPE_HEIGHT = (CANVAS_HEIGHT - PIPE_GAP) / 2 | |
const pipeBoxes = ({x, yOffset}) => [ | |
{ | |
x: x, | |
y: 0, | |
width: 100, | |
height: PIPE_HEIGHT + yOffset, | |
draw: [ | |
{fill: "green"} | |
], | |
children: [] | |
}, | |
{ | |
x: x, | |
y: PIPE_HEIGHT + PIPE_GAP + yOffset, | |
width: 100, | |
height: CANVAS_HEIGHT, | |
draw: [ | |
{fill: "green"} | |
], | |
children: [] | |
} | |
] | |
// COLLISION | |
const colliding$ = xs.empty() // TODO | |
const gameOver$ = xs.merge(outofBounds$, colliding$) | |
.fold((gameOver, newCondition) => gameOver || newCondition) | |
// RENDER | |
const renderPipe = ({x, yOffset}) => _.map(pipeBoxes({x, yOffset}), rect) | |
const debugText = state => text({ | |
x: 15, | |
y: 25, | |
value: "", | |
font: '18pt Arial', | |
draw: [ | |
{fill: 'white'} | |
] | |
}) | |
const gameOverText = gameOver => text({ | |
x: (CANVAS_WIDTH / 2) - 200, | |
y: (CANVAS_HEIGHT / 2) + 10, | |
value: gameOver ? "Game Over" : "", | |
font: '70pt Arial', | |
draw: [ | |
{fill: 'white'} | |
] | |
}) | |
const bird = y => rect({ | |
x: 100, | |
y: y, | |
width: BIRD_WIDTH, | |
height: BIRD_HEIGHT, | |
draw: [ | |
{fill: "orange"} | |
] | |
}) | |
return { | |
Canvas: xs.combine(yPosition$, pipes$, gameOver$).map(([y, pipes, gameOver]) => ( | |
rect({draw: [{fill: 'skyblue'}]}, [ | |
debugText([y, pipes, gameOver]), | |
gameOverText(gameOver), | |
bird(y) | |
].concat(_.flatMap(pipes, renderPipe))) | |
)) | |
}; | |
} | |
const drivers = { | |
Canvas: makeCanvasDriver(null, {width: CANVAS_WIDTH, height: CANVAS_HEIGHT}), | |
KeyPress$: () => fromEvent(document, 'keypress'), | |
MouseDown$: () => fromEvent(document, 'mousedown') | |
}; | |
run(main, drivers); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment