Skifree is a Windows 95 game about avoiding trees until a yeti eats you. I tried rebuilding it in Preact with Redux and RxJS for state and stream management.
The player
Every game entity has a sprite array, coordinate objects pointing to a shared spritesheet:
const sprite = [
{ name: 'w3', x: 492, y: 131, width: 90, height: 128 },
{ name: 'w2', x: 386, y: 131, width: 90, height: 128 },
{ name: 'w1', x: 280, y: 131, width: 82, height: 128 },
{ name: 'sw2', x: 158, y: 131, width: 87, height: 128 },
{ name: 'sw1', x: 88, y: 131, width: 64, height: 128 },
{ name: 'normal', x: 0, y: 0, width: 60, height: 128 },
// ... southeast, east, ouch, jump, flip1, flip2, getup
];The trees, rocks, yeti, and every other entity each have their own array like this, all pointing into the same image.

The full game spritesheet.
The illusion
The skier never moves. position.x is always canvas.width / 2, position.y always 200. What changes is center, an offset that every obstacle reads to figure out where to draw itself. In every direction.
store.dispatch({
type: 'UPDATE_CENTER',
payload: state.game.center + state.speed.x
});It’s a trick used in most side-scrollers. The mountain comes to the skier.
The obstacles
Each frame a random number decides what appears on the slope:
add(y = 0) {
const n = Math.random();
if (n > 0.80) new Tree(y, ...);
else if (n > 0.50) new Rock(y, ...);
else if (n > 0.30) new Snow(y, ...);
else if (n > 0.10 && !this.obstacles.has('ramp')) new Ramp(y, ...);
else if (n > 0.05) new Post(y, ...);
else if ( !this.obstacles.has('cart')) new Cart(y, ...);
}Trees at 20%, rocks at 30%, snow mounds at 20% (those bounce you up instead of crashing you). Ramps send you into the air with a jump strength of 100 instead of the usual 25. Only one ramp and one dog sled can exist at a time. When something scrolls off the top it gets deleted and add() fires again.
The yeti
After 60 seconds a Yeti appears and follows the player:
let speedX = (this.position.x <= playerX) ?
(6 - state.speed.x) / 8 :
-(4 - state.speed.x) / 8;
let speedY = (this.position.y <= playerY) ?
(10 - state.speed.y) / 8 :
-(10 + state.speed.y) / 8;It catches up faster from the left than the right, and the vertical gap always closes. Ski fast enough and you can hold it off. Move slower and it eats you.
Redux
Here’s the state setup:
const reducer = combineReducers({
player: playerReducer,
game: gameReducer,
speed: speedReducer
});Three reducers. For a browser game. The speed reducer alone handles PLAYER_MOVE, PLAYER_MOUSEMOVE, PLAYER_SET_RATIO, PLAYER_JUMP, and GAME_RESET.
It made debug mode easy. store.getState() at any frame tells you everything. Stepping through actions in Redux DevTools to track down a yeti glitch was satisfying.
In retrospective, canvas.addEventListener would have been fine for this game.
Press F.


Comments
Nothing yet. Say the first thing.
Sign in to join the conversation.