Skip to content

Commit

Permalink
snooker
Browse files Browse the repository at this point in the history
  • Loading branch information
tailuge committed Oct 24, 2023
1 parent 4f81059 commit fa98ff6
Show file tree
Hide file tree
Showing 11 changed files with 160 additions and 71 deletions.
6 changes: 4 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,11 @@ Unsophisticated billiards with spinning ball physics written in typescript.

[![Demo](https://raw.githubusercontent.com/tailuge/billiards/master/dist/images/t3.png)](https://tailuge.github.io/billiards/dist)

* In browser WebGL [nine ball demo ⬀](https://tailuge.github.io/billiards/dist)
Demos run in all major browsers and use WebGL
* [Nine ball ⬀](https://tailuge.github.io/billiards/dist)
* 4-ball [Straight pool ⬀](https://tailuge.github.io/billiards/dist?ruletype=fourteenone).
* 6-red [Snooker ⬀](https://tailuge.github.io/billiards/dist?ruletype=snooker).
* [Three cushion billiards ⬀](https://tailuge.github.io/billiards/dist?ruletype=threecushion).
* [Straight pool ⬀](https://tailuge.github.io/billiards/dist?ruletype=fourteenone).
* Inspect physics using [diagrams](https://tailuge.github.io/billiards/dist/diagrams/diagrams.html).
* Try [two player](https://tailuge.github.io/billiards/dist/lobby/lobby.html) online hosted on render.com

Expand Down
2 changes: 1 addition & 1 deletion dist/diagram.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion dist/index.js

Large diffs are not rendered by default.

49 changes: 29 additions & 20 deletions dist/multi.html
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@
<body>
<h1>tailuge/billiards multiplayer test page</h1>
local assets (for dev)
<br />
single player
<ul>
<li><a href="/dist/">single player nineball</a></li>
<li>
Expand All @@ -25,24 +27,40 @@ <h1>tailuge/billiards multiplayer test page</h1>
<a href="/dist/?ruletype=fourteenone">single player 14-1</a>
</li>
<li>
<a id="local2p">two players nineball in single window</a> for quick
testing
<a href="/dist/?ruletype=snooker">single player snooker</a>
</li>
</ul>
two player
<ul>
<li>
<a class="addwss" href="./2p.html?ruletype=nineball"
>two players nineball in single window</a
>
for quick testing
</li>
<li>
<a id="local2p3c">two players three cushion in single window</a>
<a class="addwss" href="./2p.html?ruletype=nineball"
>two players three cushion in single window</a
>
</li>
<li>
<a id="local2p14">two players 14-1 in single window</a>
<a class="addwss" href="./2p.html?ruletype=nineball"
>two players 14-1 in single window</a
>
</li>
<li>
<a id="local2ps">two players snooker in single window</a>
<a class="addwss" href="./2p.html?ruletype=nineball"
>two players snooker in single window</a
>
</li>
<li><a id="local" href="lobby/lobby.html?mode=local">local lobby</a></li>
<li><a href="lobby/lobby.html?mode=local">local lobby</a></li>
</ul>
github assets and local websocket server:
<ul>
<li>
<a id="github" href="https://tailuge.github.io/billiards/dist/"
<a
class="addwss"
href="https://tailuge.github.io/billiards/dist/?ruletype=nineball"
>single player</a
>
</li>
Expand All @@ -57,18 +75,9 @@ <h1>tailuge/billiards multiplayer test page</h1>
const server = window.location.origin
const wss = server.replace(/^http/, "ws") + "/ws"
const wssparam = `websocketserver=${wss}`
document.getElementById("local2p").href = `/dist/2p.html?${wssparam}`
document.getElementById(
"local2p3c"
).href = `/dist/2p.html?${wssparam}&ruletype=threecushion`
document.getElementById(
"local2p14"
).href = `/dist/2p.html?${wssparam}&ruletype=fourteenone`
document.getElementById(
"local2ps"
).href = `/dist/2p.html?${wssparam}&ruletype=snooker`
document.getElementById(
"github"
).href = `https://tailuge.github.io/billiards/dist/?${wssparam}`
const elts = document.getElementsByClassName("addwss")
for (elt of elts) {
elt.href += `&${wssparam}`
}
</script>
</html>
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@
"@testing-library/dom": "^9.3.3",
"@types/chai": "^4.3.9",
"@types/jest": "^29.5.6",
"@types/node": "^20.8.7",
"@types/node": "^20.8.8",
"@types/three": "^0.157.2",
"chai": "^4.3.10",
"ini": "^4.1.1",
Expand Down
29 changes: 1 addition & 28 deletions src/controller/rules/snooker.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,7 @@
import { WatchEvent } from "../../events/watchevent"
import { Ball, State } from "../../model/ball"
import { Outcome, OutcomeType } from "../../model/outcome"
import { R } from "../../model/physics/constants"
import { Table } from "../../model/table"
import { Rack } from "../../utils/rack"
import { TableGeometry } from "../../view/tablegeometry"
import { Controller } from "../controller"
import { NineBall } from "./nineball"
import { Rules } from "./rules"
Expand Down Expand Up @@ -46,30 +43,6 @@ export class Snooker extends NineBall implements Rules {
.map((o) => o.ballA!)
.filter((ball) => ball.id < 7)
.filter((ball) => ball.id !== 0)
.map((ball) => this.respot(ball))
}

respot(ball: Ball) {
const positions = Rack.snookerColourPositions()
positions.push(positions[ball.id - 1])
positions.reverse()
// add positions as close as possible to spot behind, then infront
let pos = positions[0].clone()
for (let x = pos.x; x < TableGeometry.tableX; x += R / 4) {
positions.push(pos.setX(x).clone())
}
pos = positions[0].clone()
for (let x = pos.x; x > -TableGeometry.tableX; x -= R / 4) {
positions.push(pos.setX(x).clone())
}
positions.some((p) => {
if (!this.container.table.overlapsAny(p, ball)) {
ball.pos.copy(p)
ball.state = State.Stationary
return true
}
return false
})
return ball
.map((ball) => Rack.respot(ball, this.container.table))
}
}
12 changes: 10 additions & 2 deletions src/events/recorder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,16 @@ export class Recorder {
return this.state(this.states[0], this.shots)
}

last() {
let last = this.states.length - 1
if (last > 0 && this.shots[last].type === "RERACK") {
last--
}
return last
}

lastShot() {
const last = this.states.length - 1
let last = this.last()
return this.state(this.states[last], [this.shots[last]])
}

Expand Down Expand Up @@ -78,7 +86,7 @@ export class Recorder {
}

if (this.breakStart === undefined) {
this.breakStart = this.states.length - 1
this.breakStart = this.last()
}
}

Expand Down
4 changes: 2 additions & 2 deletions src/model/ball.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ export class Ball {
static id = 0
readonly id = Ball.id++

private readonly transition = 0.05
static readonly transition = 0.05

constructor(pos, color?) {
this.pos = pos.clone()
Expand Down Expand Up @@ -96,7 +96,7 @@ export class Ball {
return (
this.vel.lengthSq() !== 0 &&
this.rvel.lengthSq() !== 0 &&
surfaceVelocityFull(this.vel, this.rvel).length() < this.transition
surfaceVelocityFull(this.vel, this.rvel).length() < Ball.transition
)
}

Expand Down
31 changes: 31 additions & 0 deletions src/utils/rack.ts
Original file line number Diff line number Diff line change
Expand Up @@ -169,4 +169,35 @@ export class Rack {
positions.push(new Vector3(black, 0, 0))
return positions
}

static respot(ball: Ball, table: Table) {
const positions = Rack.snookerColourPositions()
positions.push(positions[ball.id - 1])
positions.reverse()

const placed = positions.some((p) => {
if (!table.overlapsAny(p, ball)) {
ball.pos.copy(p)
ball.state = State.Stationary
return true
}
return false
})
if (!placed) {
Rack.respotBehind(positions[0], ball, table)
}
return ball
}

static respotBehind(targetpos, ball, table) {
const pos = targetpos.clone()
while (pos.x < TableGeometry.tableX && table.overlapsAny(pos, ball)) {
pos.x += R / 8
}
while (pos.x > -TableGeometry.tableX && table.overlapsAny(pos, ball)) {
pos.x -= R / 8
}
ball.pos.copy(pos)
ball.state = State.Stationary
}
}
68 changes: 67 additions & 1 deletion test/rules/snooker.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,17 @@ import { expect } from "chai"
import { Container } from "../../src/container/container"
import { GameEvent } from "../../src/events/gameevent"
import { initDom } from "../view/dom"
import { BeginEvent } from "../../src/events/beginevent"
import { Input } from "../../src/events/input"
import { PocketGeometry } from "../../src/view/pocketgeometry"
import { R } from "../../src/model/physics/constants"
import { Ball, State } from "../../src/model/ball"
import { Vector3 } from "three"
import { PlayShot } from "../../src/controller/playshot"
import { Aim } from "../../src/controller/aim"
import { WatchEvent } from "../../src/events/watchevent"
import { RerackEvent } from "../../src/events/rerackevent"
import { PlaceBall } from "../../src/controller/placeball"

initDom()

Expand All @@ -22,14 +33,69 @@ describe("Snooker", () => {
const rule = "snooker"

beforeEach(function (done) {
Ball.id = 0
container = new Container(undefined, (_) => {}, false, rule)
broadcastEvents = []
container.broadcast = (x) => broadcastEvents.push(x)
done()
})

it("Fourteenone has 13 balls", (done) => {
function bringToAimMode() {
container.eventQueue.push(new BeginEvent())
container.processEvents()
expect(container.controller).to.be.an.instanceof(PlaceBall)
container.inputQueue.push(new Input(0.1, "SpaceUp"))
container.processEvents()
expect(container.controller).to.be.an.instanceof(Aim)
container.advance(1)
container.processEvents()
}

const edge =
PocketGeometry.pockets.pocketS.pocket.pos.y +
PocketGeometry.middleRadius +
0.01 * R

function setupTableWithPot(ball) {
container.table.cueball.pos.copy(new Vector3(0, edge + R * 2.1, 0))
ball.pos.copy(new Vector3(0, edge, 0))
}

function playShotWaitForOutcome() {
container.table.cue.aim.angle = -Math.PI / 2
container.table.cue.aim.power = 1
container.table.cue.aim.pos.copy(container.table.balls[0].pos)
container.inputQueue.push(new Input(0.1, "SpaceUp"))
container.processEvents()
expect(container.controller).to.be.an.instanceof(PlayShot)
container.advance(1)
container.processEvents()
}

it("Snooker has 6 colours and 6 reds", (done) => {
expect(container.table.balls).to.be.length(13)
done()
})

it("Pot red", (done) => {
bringToAimMode()
expect(container.controller).to.be.an.instanceof(Aim)

setupTableWithPot(container.table.balls[7])
playShotWaitForOutcome()
expect(container.controller).to.be.an.instanceof(Aim)
expect(container.recoder.shots).to.be.length(1)
done()
})

it("Pot colour is respotted", (done) => {
bringToAimMode()
expect(container.controller).to.be.an.instanceof(Aim)

setupTableWithPot(container.table.balls[6])
playShotWaitForOutcome()
expect(container.controller).to.be.an.instanceof(Aim)
expect(container.recoder.shots).to.be.length(2)
done()
})
})
26 changes: 13 additions & 13 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -929,10 +929,10 @@
resolved "https://registry.yarnpkg.com/@types/mime/-/mime-1.3.4.tgz#a4ed836e069491414bab92c31fdea9e557aca0d9"
integrity sha512-1Gjee59G25MrQGk8bsNvC6fxNiRgUlGn2wlhGf95a59DrprnnHk80FIMMFG9XHMdrfsuA119ht06QPDXA1Z7tw==

"@types/node@*", "@types/node@^20.8.7":
version "20.8.7"
resolved "https://registry.yarnpkg.com/@types/node/-/node-20.8.7.tgz#ad23827850843de973096edfc5abc9e922492a25"
integrity sha512-21TKHHh3eUHIi2MloeptJWALuCu5H7HQTdTrWIFReA8ad+aggoX+lRes3ex7/FtpC+sVUpFMQ+QTfYr74mruiQ==
"@types/node@*", "@types/node@^20.8.8":
version "20.8.8"
resolved "https://registry.yarnpkg.com/@types/node/-/node-20.8.8.tgz#adee050b422061ad5255fc38ff71b2bb96ea2a0e"
integrity sha512-YRsdVxq6OaLfmR9Hy816IMp33xOBjfyOgUd77ehqg96CFywxAPbDbXvAsuN2KVg2HOT8Eh6uAfU+l4WffwPVrQ==
dependencies:
undici-types "~5.25.1"

Expand Down Expand Up @@ -1008,9 +1008,9 @@
integrity sha512-95Sfz4nvMAb0Nl9DTxN3j64adfwfbBPEYq14VN7zT5J5O2M9V6iZMIIQU1U+pJyl9agHYHNCqhCXgyEtIRRa5A==

"@types/webxr@*":
version "0.5.6"
resolved "https://registry.yarnpkg.com/@types/webxr/-/webxr-0.5.6.tgz#835c7ac9983a732e2e849d0d302bc735aa455126"
integrity sha512-/uWg82/WT+Pl18b2kkG6nlbiiaNIb8RN2mvvcGexGvwLvUrEhDhGBzYHiwa5nQPtin0hISyrXkKOKVScTK+kKg==
version "0.5.7"
resolved "https://registry.yarnpkg.com/@types/webxr/-/webxr-0.5.7.tgz#7a3aaaf1ceeaaad13f1dda66d6571852405bb221"
integrity sha512-Rcgs5c2eNFnHp53YOjgtKfl/zWX1Y+uFGUwlSXrWcZWu3yhANRezmph4MninmqybUYT6g9ZE0aQ9QIdPkLR3Kg==

"@types/ws@^8.5.5":
version "8.5.8"
Expand Down Expand Up @@ -1571,9 +1571,9 @@ camelcase@^6.0.0, camelcase@^6.2.0:
integrity sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==

caniuse-lite@^1.0.30001541:
version "1.0.30001551"
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001551.tgz#1f2cfa8820bd97c971a57349d7fd8f6e08664a3e"
integrity sha512-vtBAez47BoGMMzlbYhfXrMV1kvRF2WP/lqiMuDu1Sb4EE4LKEgjopFDSRtZfdVnslNRpOqV/woE+Xgrwj6VQlg==
version "1.0.30001553"
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001553.tgz#e64e7dc8fd4885cd246bb476471420beb5e474b5"
integrity sha512-N0ttd6TrFfuqKNi+pMgWJTb9qrdJu4JSpgPFLe/lrD19ugC6fZgF0pUewRowDwzdDnb9V41mFcdlYgl/PyKf4A==

chai@^4.3.10:
version "4.3.10"
Expand Down Expand Up @@ -2022,9 +2022,9 @@ ee-first@1.1.1:
integrity sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==

electron-to-chromium@^1.4.535:
version "1.4.563"
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.563.tgz#dabb424202754c1fed2d2938ff564b23d3bbf0d3"
integrity sha512-dg5gj5qOgfZNkPNeyKBZQAQitIQ/xwfIDmEQJHCbXaD9ebTZxwJXUsDYcBlAvZGZLi+/354l35J1wkmP6CqYaw==
version "1.4.565"
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.565.tgz#205f3746a759ec3c43bce98b9eef5445f2721ea9"
integrity sha512-XbMoT6yIvg2xzcbs5hCADi0dXBh4//En3oFXmtPX+jiyyiCTiM9DGFT2SLottjpEs9Z8Mh8SqahbR96MaHfuSg==

emittery@^0.13.1:
version "0.13.1"
Expand Down

0 comments on commit fa98ff6

Please sign in to comment.