import { todo } from './todo.js' import { Action, pointEquals, opposite } from './model.js' import { move } from './rules.js' import { freeCells } from './spawn.js' import { pure, map, flatMap, intBelow } from './random.js' /** @import { Point, Direction, GameState, Random, CheckResult } from './types' */ /** * m12 check: property-based testing, built from the course's own parts. * A generator of test data is nothing new: it is a `Random` from random.js (`seed -> [A, nextSeed]`). */ /** * A Random that gives one of the elements of the non-empty array `xs`: the element at index `i`, * where `i` is drawn with `intBelow(xs.length)`. * @type {(xs: A[]) => Random} */ export const elementOf = (xs) => todo('m12') /** * A Random that gives an array of `n` values drawn from `gen`, one after another: the first draw starts from the * incoming seed, each later draw from the seed the previous one left behind, and the last draw's seed is the result's. * `listOf(gen, 0)` gives `[]` and leaves the seed alone. No loop: recursion on `n`, with `flatMap` and `map`. * @type {(gen: Random, n: number) => Random} */ export const listOf = (gen, n) => todo('m12') // Ticks appear three times so that the snake moves more often than it turns. const actionPool = [ Action.Tick, Action.Tick, Action.Tick, Action.Turn('Up'), Action.Turn('Down'), Action.Turn('Left'), Action.Turn('Right'), ] /** A generator of `n` random actions (given; it works once `elementOf` and `listOf` do). * @type {(n: number) => Random} */ export const genActions = (n) => listOf(elementOf(actionPool), n) /** * Check the property `prop` (a function value -> boolean) on `runs` values drawn from `gen`. * The first value is drawn from `seed`, each later one from the seed the previous draw left behind. * Stop at the first value for which `prop` is false. * Result: `{ ok: true, runs }` when every value passed, otherwise `{ ok: false, run, seed, value }`, where * `run` is the 0-based number of the failing draw, `value` is the counterexample and `seed` is the seed that * draw started from (so `run(gen, seed)[0]` gives `value` again). * @type {(gen: Random, prop: (a: A) => boolean, runs: number, seed: number) => CheckResult} */ export const forAll = (gen, prop, runs, seed) => todo('m12') /** * Every list that is `xs` with exactly one element removed, in the order of the removed index: * `removals([1, 2, 3])` is `[[2, 3], [1, 3], [1, 2]]`; `removals([])` is `[]`. * @type {(xs: A[]) => A[][]} */ export const removals = (xs) => todo('m12') /** * Shrink a failing list. `fails` is a function list -> boolean and `fails(xs)` is true. * If some list in `removals(xs)` still fails, continue with the first one that does; when none fails, return `xs`. * The result is "1-minimal": it fails, and removing any single element makes it pass. * @type {(xs: A[], fails: (xs: A[]) => boolean) => A[]} */ export const minimize = (xs, fails) => todo('m12') /** * The names of the invariants that `state` breaks, in this order (an empty array means the state is sane): * 'snake-not-empty' the snake has at least one cell * 'snake-in-bounds' every cell of the snake is on the board * 'snake-no-overlap' no two cells of the snake are equal * 'snake-connected' each cell of the snake is exactly one step (up, down, left or right) from the one before it * 'apple-in-bounds' the apple, if there is one, is on the board * 'apple-not-on-snake' the apple, if there is one, is not on a cell of the snake * 'turn-not-reversed' `nextDir` is not the opposite of `dir` * 'queued-not-reversed' `queued` is null, or is not the opposite of `nextDir` * @type {(state: GameState) => string[]} */ export const violations = (state) => todo('m12') /** * Given the states of a game in order, the snake "moves at index i" (i >= 1) when the head of `states[i]` * differs from the head of `states[i - 1]`. `moveGaps(states)` is the list of differences between consecutive * such indices: moves at 2, 5 and 12 give `[3, 7]`. Fewer than two moves give `[]`. * @type {(states: GameState[]) => number[]} */ export const moveGaps = (states) => todo('m12') /** * (Exercise 12.6) A Random that gives a snake, head first, on a `width` x `height` board, with at most `maxLen` cells * (`maxLen >= 1`). The draws, in this exact order (so that every track gets the same snake from the same seed): * 1. the head: `x` with `intBelow(width)`, then `y` with `intBelow(height)`; * 2. then, while the snake has fewer than `maxLen` cells: look at the neighbours of its *last* cell, in the order * Up, Down, Left, Right (`move(last, dir)`), and keep those that are on the board and not yet in the snake. * If none is left, stop early. Otherwise append the one picked with `elementOf(neighbours)` and go on. * So the result is always on the board, connected and free of overlap. No loop: recursion, with `flatMap`. * @type {(width: number, height: number, maxLen: number) => Random} */ export const genSnake = (width, height, maxLen) => todo('m12') /** @type {Direction[]} */ const directions = ['Up', 'Down', 'Left', 'Right'] // The direction the head moved in to get from the second cell to the first ('Right' for a one-cell snake). const headingOf = (snake) => snake.length < 2 ? 'Right' : /** @type {Direction} */ (directions.find((d) => pointEquals(move(snake[1], d), snake[0]))) /** * A random sane Playing state on a `width` x `height` board (given; it works once `genSnake` does). Draws in order: * the snake's length limit `n + 1` with `n` from `intBelow(width * height)`; the snake with `genSnake`; `nextDir` with * `elementOf` among Up, Down, Left, Right without the opposite of `dir` (so: `dir` or a perpendicular direction); * a coin `intBelow(2)`: 0 means no apple, 1 means an apple at `elementOf(freeCells)` (no apple if no cell is free). * `dir` is the direction from the snake's second cell to its head ('Right' for a one-cell snake); seed, score and * frame are 0, `queued` is null. * @type {(width: number, height: number) => Random} */ export const genState = (width, height) => flatMap(intBelow(width * height), (n) => flatMap(genSnake(width, height, n + 1), (snake) => { const dir = headingOf(snake) const free = freeCells({ width, height, snake }) return flatMap(elementOf(directions.filter((d) => d !== opposite(dir))), (nextDir) => flatMap(intBelow(2), (coin) => map(coin === 0 || free.length === 0 ? pure(null) : elementOf(free), (apple) => ({ width, height, snake, dir, nextDir, apple, seed: 0, score: 0, status: 'Playing', frame: 0, queued: null, })))) })) /** * (Exercise 12.7) The step of Module 3, written straight from the rules, as a second opinion on `advance`. * Not Playing: the state unchanged. Otherwise build the snake the step would produce: the new head * (`move(head, nextDir)`) in front, followed by the old snake, without its last cell unless the new head is on the * apple. Then ask `violations` about the state with that snake: if the list contains 'snake-in-bounds' or * 'snake-no-overlap', the result is Module 3's game over (`dir: nextDir`, status 'GameOver', nothing else changed); * otherwise the state with the new snake, `dir: nextDir`, and, when eating, the apple gone and the score one higher. * Do not call `advance`. On every sane state the result must equal `advance(state)`. * @type {(state: GameState) => GameState} */ export const advanceSpec = (state) => todo('m12')