Skip to content

Instantly share code, notes, and snippets.

@stevekrouse
Created April 10, 2018 10:56
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save stevekrouse/85f350eb82ebbf5b2c244aa9aeffc453 to your computer and use it in GitHub Desktop.
Save stevekrouse/85f350eb82ebbf5b2c244aa9aeffc453 to your computer and use it in GitHub Desktop.
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