Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,15 @@ turn it is after each change, and the result and race-to-3 line name the players
The tests pin that the turn follows the shooter, that a foul passes it on, and that the AI never
shoots for a human (mutation-checked).

## Lobby and the tutorial

The lobby is grouped instead of a row of buttons: a primary row (**Find a game**, **Practice vs AI**, **Two players, one device**), the game settings (game, AI level, table, **House rules**), then
**Sharpen your game** (*How to play*, *Daily shot*, *Drills and trick shots*, *Challenges*, *Puzzle maker*) and **Solo runs** (*Career*, *Rogue Pool*).

A player who has never opened the tutorial sees a *New here?* banner once. **How to play** is five short steps, each an ordinary drill dressed with the words for one control: aim and shoot, power, cutting a ball,
spin, and bank shots. Finishing (or *Not now*) is remembered in the browser and the banner does not return; *How to play* stays available. The steps are the drills' own, so progress is too, and Show me works on each.
`src/tutorial.js` is the steps as data; the tests solve every step, check the lobby keeps one id per control and that each one still has a handler.

## Trick shots

**Drills** now also lists twenty curated **trick shots**: small tables that one shot clears completely (eight with two balls, eight with three, four with four), easiest first. The win rule is `clear`:
Expand Down
9 changes: 8 additions & 1 deletion public/feature.html
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,7 @@ <h2><span class="n">01</span>The field</h2>
</section>

<section>
<h2><span class="n">02</span>Shipped <small style="color:var(--muted2);font-weight:500">(21)</small></h2>
<h2><span class="n">02</span>Shipped <small style="color:var(--muted2);font-weight:500">(22)</small></h2>
<p class="section-sub">Everything below is live. Each card says what it does and, where it matters, what it does not do yet.</p>
<div class="roadmap">

Expand Down Expand Up @@ -294,6 +294,13 @@ <h2><span class="n">02</span>Shipped <small style="color:var(--muted2);font-weig
<span class="tag done">Shipped</span>
</div>

<div class="rm shipped">
<div class="rank">✓</div>
<div><h4>A tidier lobby and a first-run tutorial</h4>
<p><strong>Shipped.</strong> The lobby is grouped: a primary row (Find a game, Practice vs AI, Two players on one device), the game settings (game, AI, table, house rules), then Sharpen your game (How to play, Daily shot, Drills and trick shots, Challenges, Puzzle maker) and Solo runs (Career, Rogue Pool). A new player is offered a five-step tutorial once: aim, power, cutting, spin and banks, each an ordinary drill with a plain explanation and a Show me.</p></div>
<span class="tag done">Shipped</span>
</div>

<div class="rm shipped">
<div class="rank">✓</div>
<div><h4>Curated trick shots</h4>
Expand Down
17 changes: 11 additions & 6 deletions src/main.js

Large diffs are not rendered by default.

13 changes: 13 additions & 0 deletions src/style.css

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

30 changes: 30 additions & 0 deletions src/tutorial.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
// The first-run tutorial: five short steps, each one an ordinary drill with a plain explanation of the control it
// teaches, so a new player learns the game by doing it. Pure data and a few helpers; main.js plays the steps as drills.

export const STEPS=[
{drill:'straight-in',title:'Aim and shoot',
text:'Drag on the table to turn the cue. The dotted line shows where the ball will go: point it at the yellow ball and the pocket, then press Shoot (or Space).'},
{drill:'across-the-table',title:'Power',
text:'The Power slider is how hard you hit. Harder is faster but harder to control. This ball is a long way off, so try about half power.'},
{drill:'side-cut',title:'Cutting a ball',
text:'You rarely hit a ball straight on. Aim at the far edge of it to send it off at an angle toward the pocket; the second line shows where it will travel.'},
{drill:'stop-shot',title:'Spin',
text:'The white circle beside Shoot is the cue ball. Tap low on it for backspin, high for follow-through. Here, hit low and centre so the cue ball stops where the yellow ball was.'},
{drill:'bank-shot',title:'Bank shots',
text:'A ball can bounce off a cushion. Aim the ball at the rail, not at the pocket; the bent line shows the bank. Stuck? Press Show me.'}
]

export const DONE_KEY='pool-masters:tutorial'
export const stepAt=i=>Number.isInteger(i)&&i>=0&&i<STEPS.length?STEPS[i]:null
export const hasNext=i=>Number.isInteger(i)&&i>=0&&i<STEPS.length-1
export const label=i=>`TUTORIAL ${i+1} OF ${STEPS.length}`

// Whether to offer the tutorial to someone: only until they have done it or said no.
export const shouldOffer=stored=>stored==null||stored===''

// A drill, dressed as a tutorial step: the same layout, rule and hint, with the step's own words.
export function stepDrill(drills,i){
const s=stepAt(i);if(!s)return null
const d=drills.find(x=>x.id===s.drill);if(!d)return null
return {...d,name:s.title,tip:s.text,label:label(i),tutorial:i}
}
43 changes: 43 additions & 0 deletions test/tutorial.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import test from 'node:test';import assert from 'node:assert/strict'
import fs from 'node:fs'
import {STEPS,stepAt,hasNext,label,shouldOffer,stepDrill,DONE_KEY} from '../src/tutorial.js'
import {DRILLS,attempt} from '../src/drills.js'
import {setTableSize} from '../src/table.js'
setTableSize(7)

test('every step teaches with a real drill, in plain words, and can be solved',()=>{
assert.equal(STEPS.length,5)
for(let i=0;i<STEPS.length;i++){
const d=stepDrill(DRILLS,i),src=DRILLS.find(x=>x.id===STEPS[i].drill)
assert.ok(d&&src,`step ${i}: drill ${STEPS[i].drill} exists`)
assert.equal(d.tutorial,i);assert.equal(d.tip,STEPS[i].text);assert.equal(d.name,STEPS[i].title);assert.equal(d.label,`TUTORIAL ${i+1} OF 5`)
assert.equal(d.id,src.id,'same id, so progress is the drill\'s own');assert.deepEqual(d.layout,src.layout);assert.deepEqual(d.win,src.win)
assert.equal(attempt(d,d.hint).ok,true,`${d.id}: Show me solves it`)
assert.ok(STEPS[i].text.length>40&&STEPS[i].text.length<260,'a sentence or two')
}
assert.equal(new Set(STEPS.map(s=>s.drill)).size,STEPS.length,'no drill twice')
})

test('the steps run in order, the last one has no next, and out-of-range asks are refused',()=>{
assert.equal(hasNext(0),true);assert.equal(hasNext(STEPS.length-2),true);assert.equal(hasNext(STEPS.length-1),false)
for(const bad of [-1,5,1.5,null,undefined,'2',NaN])assert.equal(hasNext(bad),false,String(bad))
assert.equal(stepAt(0).title,'Aim and shoot');assert.equal(stepAt(5),null);assert.equal(stepAt(-1),null);assert.equal(stepDrill(DRILLS,9),null)
assert.equal(label(2),'TUTORIAL 3 OF 5')
})

test('it is offered to a new player once, and never again after they finish or decline',()=>{
assert.equal(shouldOffer(null),true);assert.equal(shouldOffer(undefined),true);assert.equal(shouldOffer(''),true)
assert.equal(shouldOffer('done'),false);assert.equal(shouldOffer('skipped'),false)
assert.match(DONE_KEY,/^pool-masters:/)
})

test('the lobby is grouped, every control keeps one id, and nothing the code binds went missing',()=>{
const src=fs.readFileSync(new URL('../src/main.js',import.meta.url),'utf8')
const a=src.indexOf('<section id="lobby"'),lobby=src.slice(a,src.indexOf('</section>',a))
const ids=[...lobby.matchAll(/ id="([^"]+)"/g)].map(m=>m[1])
assert.equal(new Set(ids).size,ids.length,'no duplicate ids')
for(const id of ['quick','practice','hotseat','game-mode','ai-level','obstacles','house','tutorial','daily','drills','challenges','puzzles','career','rogue','code','join','create','learn','learn-go','learn-skip','rooms','leaders','refresh'])assert.ok(ids.includes(id),`#${id} is on the lobby`)
for(const g of ['Sharpen your game','Solo runs'])assert.ok(lobby.includes(g),g)
// every button the old lobby had is still reachable, and each is bound
for(const id of ['quick','practice','hotseat','house','daily','drills','challenges','puzzles','career','rogue','tutorial'])assert.ok(src.includes(`$('#${id}').onclick`),`#${id} has a handler`)
})
1 change: 1 addition & 0 deletions tools/build-roadmap.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ const shippedNew=[
['Aim snapping and a follow-the-shot camera','Aim snaps to the middle of a pocket (or a one-cushion bank) when you are close, can be turned off, and the 3D camera can follow the ball with an adjustable eye height.'],
['Rogue Pool','A solo roguelike run: clear each table within its shots, take one of three random upgrades (wider pockets, extra shots, lives, jump charges, a scratch shield, a second wind), and lose a life when you run out. The same seed is the same run.'],
['Chaos Pool','A scored game to 15 with a random twist dealt before every shot: a bonus pocket worth three, a bomb ball that blasts its neighbours, or a gravity well that bends the balls. The host deals it, so both players see the same one.'],
['A tidier lobby and a first-run tutorial','The lobby is grouped: a primary row (Find a game, Practice vs AI, Two players on one device), the game settings (game, AI, table, house rules), then Sharpen your game (How to play, Daily shot, Drills and trick shots, Challenges, Puzzle maker) and Solo runs (Career, Rogue Pool). A new player is offered a five-step tutorial once: aim, power, cutting, spin and banks, each an ordinary drill with a plain explanation and a Show me.'],
['Curated trick shots','Twenty tables that one shot clears completely (eight with two balls, eight with three, four with four), under Drills. Each has a Show me button that sets up and plays the proven shot. Every one was found by searching the real physics, and the tests replay each through the physics, so a change that breaks one fails the build.'],
['Obstacle tables','A table option in the lobby for practice and two players on one device: Bumpers, Walls, Portals or the Gauntlet. Balls bounce off bumpers and walls and come out of the paired portal going the same way; a jump shot hops a bumper or wall. The AI, the solver and the physics all see them. Not yet: online tables, and placing your own in the puzzle maker.'],
['Puzzle maker','Arrange a table, pick the ball to pot, and the computer searches the real physics for a shot. Only proven-solvable tables can be shared, and a puzzle link re-proves itself when it is opened.']
Expand Down
Loading