agent-timelines 0.2.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,211 @@
1
+ /* Procedural dungeon for the agent tree: one room per node, one corridor per parent->child edge.
2
+ * Layout is append-only: room i depends only on seed, node i and rooms 0..i-1, in world coordinates
3
+ * (root at 0,0, may be negative). tiles[y][x] is world cell (x+ox, y+oy); rooms/paths/deco use world coords.
4
+ * Every random look choice hashes (seed, room id) or (seed, world cell), so appending nodes never redecorates.
5
+ * Plain ES5 syntax (Math.imul aside); window.Dungeon, and module.exports for Node. */
6
+ (function (root) {
7
+ 'use strict';
8
+ // Sprite indices are row*cols+col, 16px. tiles[][] are all 'td'; deco entries name their sheet.
9
+ var COLS = { td: 12, ob: 48 }; // td = Kenney Tiny Dungeon, ob = Kenney 1-Bit Pack (colored)
10
+ var FLOOR = 48, FLOOR_SPECK = 49, FLOOR_STONES = 42; // td sand: plain, specks, loose stones (rooms)
11
+ var CORRIDOR = 0, CORR_HOLES = 12, CORR_RUBBLE = 24; // td brown dirt: plain, holes, rubble (corridors)
12
+ var DOOR = 45; // td wooden door in a dark stone frame
13
+ var WALL = 40; // td grey brick wall face (walkable floor directly south)
14
+ var WALL_GRATE = 28; // td brick face with a barred grate (rare face variant)
15
+ var WALL_TOP = 37; // td wall cap, horizontal stone bars, wall on both sides
16
+ var TOP_L = 36, TOP_R = 38, TOP_LR = 39; // td wall cap with a dark outline on the left / right / both ends
17
+ var BANNER = 29; // td red and gold shield banner hung on a brick face
18
+ var HEARTH_L = 10, HEARTH_R = 11; // td two-wide dark fireplace arch with glowing embers (left, right half)
19
+ var TABLE = 72; // td wooden table
20
+ var STOOL = 73; // td small wooden stool
21
+ var BARREL = 82; // td banded wooden barrel
22
+ var CRATE = 67; // td wooden crate with iron bands
23
+ var CHEST = 89; // td closed treasure chest
24
+ var CABINET = 63, CABINET_B = 75; // td tall wooden shelf/cabinets with drawers (two variants)
25
+ var OB_SKULL = 720; // ob skull and crossbones
26
+ var OB_BONES = 721; // ob bone pile
27
+ var OB_WEB = 722; // ob cobweb
28
+ var OB_CANDLE = 724; // ob lit candle (used as a wall sconce and on the table)
29
+ var OB_CANDELABRA = 725; // ob three-armed lit candelabra
30
+ var OB_FLAME = 495; // ob orange flame, burning inside the hearth arch
31
+ var OB_NOTICE = 352; // ob notice board on legs
32
+ var OB_RUG_L = 629, OB_RUG = 630, OB_RUG_R = 631; // ob red patterned carpet: left end, middle, right end
33
+ var WALK = [FLOOR, FLOOR_SPECK, FLOOR_STONES, CORRIDOR, CORR_HOLES, CORR_RUBBLE, DOOR];
34
+ var DEF = { minW: 5, maxW: 9, minH: 4, maxH: 7, rootW: 15, rootH: 11 };
35
+
36
+ function prng(str) { // FNV-1a string hash -> mulberry32
37
+ var a = 2166136261, i;
38
+ for (i = 0; i < str.length; i++) a = Math.imul(a ^ str.charCodeAt(i), 16777619);
39
+ return function () {
40
+ a = (a + 0x6D2B79F5) | 0;
41
+ var t = Math.imul(a ^ a >>> 15, 1 | a);
42
+ t = t + Math.imul(t ^ t >>> 7, 61 | t) ^ t;
43
+ return ((t ^ t >>> 14) >>> 0) / 4294967296;
44
+ };
45
+ }
46
+ function clamp(v, lo, hi) { return Math.max(lo, Math.min(hi, v)); }
47
+ function inBox(q, x, y, pad) { return x >= q.x - pad && x < q.x + q.w + pad && y >= q.y - pad && y < q.y + q.h + pad; }
48
+
49
+ // Door cell in a's wall facing b, plus the free cell just outside it. Doors never sit at a wall's ends
50
+ // (the hall: only its middle 5 cells), so corner and wall props placed earlier can never block a later door.
51
+ function door(a, b) {
52
+ var dx = b.cx - a.cx, dy = b.cy - a.cy, m = a.hall ? 2 : a.w + a.h;
53
+ var hz = Math.abs(dx) * (a.h + b.h) > Math.abs(dy) * (a.w + b.w);
54
+ var sx = hz ? (dx > 0 ? 1 : -1) : 0, sy = hz ? 0 : (dy > 0 ? 1 : -1);
55
+ var x = hz ? (sx > 0 ? a.x + a.w : a.x - 1) : clamp(b.cx, Math.max(a.x + 1, a.cx - m), Math.min(a.x + a.w - 2, a.cx + m));
56
+ var y = hz ? clamp(b.cy, Math.max(a.y + 1, a.cy - m), Math.min(a.y + a.h - 2, a.cy + m)) : (sy > 0 ? a.y + a.h : a.y - 1);
57
+ return { d: [x, y], o: [x + sx, y + sy] };
58
+ }
59
+
60
+ // BFS between two door-outside cells around every room's wall ring; null if sealed in.
61
+ function route(rooms, A, B) {
62
+ var x0 = 1e9, y0 = 1e9, x1 = -1e9, y1 = -1e9, i;
63
+ for (i = 0; i < rooms.length; i++) {
64
+ x0 = Math.min(x0, rooms[i].x - 6); y0 = Math.min(y0, rooms[i].y - 6);
65
+ x1 = Math.max(x1, rooms[i].x + rooms[i].w + 6); y1 = Math.max(y1, rooms[i].y + rooms[i].h + 6);
66
+ }
67
+ var W = x1 - x0, prev = {}, q = [A], head = 0, dirs = [[1, 0], [-1, 0], [0, 1], [0, -1]];
68
+ function key(c) { return (c[1] - y0) * W + c[0] - x0; }
69
+ prev[key(A)] = null;
70
+ while (head < q.length) {
71
+ var c = q[head++];
72
+ if (c[0] === B[0] && c[1] === B[1]) {
73
+ var out = [];
74
+ for (; c; c = prev[key(c)]) out.unshift(c);
75
+ return out;
76
+ }
77
+ for (i = 0; i < 4; i++) {
78
+ var n = [c[0] + dirs[i][0], c[1] + dirs[i][1]], j, bad = n[0] < x0 || n[0] >= x1 || n[1] < y0 || n[1] >= y1 || key(n) in prev;
79
+ for (j = 0; !bad && j < rooms.length; j++) bad = inBox(rooms[j], n[0], n[1], 1);
80
+ if (!bad) { prev[key(n)] = c; q.push(n); }
81
+ }
82
+ }
83
+ return null;
84
+ }
85
+
86
+ // Props for one room, from seed + room geometry only. Agent rooms: interior corners and the two end
87
+ // cells of the top wall face, which door() never uses. Hall: everything stays off its door zones.
88
+ function decorate(seed, q) {
89
+ var out = [], X0 = q.x, Y0 = q.y, X1 = q.x + q.w - 1, Y1 = q.y + q.h - 1, cx = q.cx, cy = q.cy, x, y;
90
+ function put(x, y, sheet, idx, solid) {
91
+ if (x >= X0 && x <= X1 && y >= Y0 - 1 && y <= Y1) out.push({ x: x, y: y, sheet: sheet, idx: idx, solid: !!solid });
92
+ }
93
+ if (q.hall) {
94
+ [X0, cx - 3, cx + 3, X1].forEach(function (x) { put(x, Y0 - 1, 'td', BANNER); });
95
+ put(X0 + 2, Y0 - 1, 'td', HEARTH_L); put(X0 + 3, Y0 - 1, 'td', HEARTH_R);
96
+ put(X0 + 2, Y0 - 1, 'ob', OB_FLAME); put(X0 + 3, Y0 - 1, 'ob', OB_FLAME); put(X0 + 1, Y0 - 1, 'ob', OB_NOTICE);
97
+ for (y = cy - 2; y <= cy + 2; y++) for (x = X0 + 2; x <= X1 - 2; x++) put(x, y, 'ob', x === X0 + 2 ? OB_RUG_L : x === X1 - 2 ? OB_RUG_R : OB_RUG);
98
+ for (x = cx - 3; x <= cx + 3; x++) put(x, cy, 'td', TABLE, 1);
99
+ [cx - 3, cx - 1, cx + 1, cx + 3].forEach(function (x) { put(x, cy - 1, 'td', STOOL, 1); put(x, cy + 1, 'td', STOOL, 1); });
100
+ put(cx - 4, cy, 'td', STOOL, 1); put(cx + 4, cy, 'td', STOOL, 1);
101
+ put(cx - 2, cy, 'ob', OB_CANDLE); put(cx + 2, cy, 'ob', OB_CANDLE);
102
+ for (x = X1 - 3; x < X1; x++) put(x, Y0, 'td', x % 2 ? CABINET : CABINET_B, 1);
103
+ put(X0, Y0, 'td', CRATE, 1); put(X1, Y0, 'ob', OB_CANDELABRA, 1);
104
+ put(X0, cy + 3, 'ob', OB_CANDELABRA, 1); put(X1, cy + 3, 'ob', OB_CANDELABRA, 1);
105
+ put(X0, Y1, 'td', BARREL, 1); put(X0 + 1, Y1, 'td', BARREL, 1); put(X1 - 1, Y1, 'td', CRATE, 1); put(X1, Y1, 'td', CHEST, 1);
106
+ return out;
107
+ }
108
+ var r = prng(seed + '/deco/' + q.id), props = [['td', BARREL, 1], ['td', BARREL, 1], ['td', CRATE, 1], ['td', CHEST, 1],
109
+ ['ob', OB_SKULL], ['ob', OB_BONES], ['ob', OB_CANDELABRA, 1]];
110
+ [[X0, Y0], [X1, Y0], [X0, Y1], [X1, Y1]].forEach(function (c, j) {
111
+ var v = r(), p = j < 2 && v < 0.2 ? ['ob', OB_WEB] : props[r() * props.length | 0];
112
+ if (v < 0.65) put(c[0], c[1], p[0], p[1], p[2]);
113
+ });
114
+ [X0, X1].forEach(function (x) { if (r() < 0.6) put(x, Y0 - 1, 'ob', OB_CANDLE); });
115
+ return out;
116
+ }
117
+
118
+ function generate(seed, nodes, opts) {
119
+ var o = {}, k, rooms = [], paths = [], cells = [], idx = {};
120
+ for (k in DEF) o[k] = opts && opts[k] != null ? opts[k] : DEF[k];
121
+
122
+ nodes.forEach(function (n, i) {
123
+ var r = prng(seed + '/' + n.id), par = i ? rooms[idx[n.parent] || 0] : null;
124
+ var w = o.rootW, h = o.rootH, x = -(w >> 1), y = -(h >> 1), room, p;
125
+ for (k = 0; par; k++) { // candidate spots spiral outward from the parent until one fits and routes
126
+ w = o.minW + (r() * (o.maxW - o.minW + 1) | 0); h = o.minH + (r() * (o.maxH - o.minH + 1) | 0);
127
+ var a = r() * 6.2832, g = 3 + r() * (2 + k / 4);
128
+ x = Math.round(par.cx + Math.cos(a) * ((par.w + w) / 2 + g) - w / 2);
129
+ y = Math.round(par.cy + Math.sin(a) * ((par.h + h) / 2 + g) - h / 2);
130
+ // 3 free cells (wall, lane, wall) between rooms; no corridor cell may touch the new walls
131
+ var ok = rooms.every(function (q) { return x - 3 >= q.x + q.w || x + w + 3 <= q.x || y - 3 >= q.y + q.h || y + h + 3 <= q.y; });
132
+ room = { id: n.id, x: x, y: y, w: w, h: h, cx: x + (w >> 1), cy: y + (h >> 1) };
133
+ ok = ok && !cells.some(function (c) { return inBox(room, c[0], c[1], 1); });
134
+ if (ok && (p = route(rooms.concat(room), door(par, room).o, door(room, par).o))) break;
135
+ }
136
+ room = { id: n.id, x: x, y: y, w: w, h: h, cx: x + (w >> 1), cy: y + (h >> 1) };
137
+ if (!i) room.hall = true;
138
+ if (par) {
139
+ p = [door(par, room).d].concat(p, [door(room, par).d]);
140
+ paths.push({ from: par.id, to: n.id, cells: p });
141
+ cells.push.apply(cells, p);
142
+ }
143
+ idx[n.id] = i;
144
+ rooms.push(room);
145
+ });
146
+
147
+ // Rasterise floors and doors, each floor cell's variant hashed from its world cell.
148
+ var T = {}, x0 = 1e9, y0 = 1e9, x1 = -1e9, y1 = -1e9;
149
+ function at(x, y) { return T[x + ',' + y]; }
150
+ function hash(x, y) { return prng(seed + '#' + x + ',' + y)(); }
151
+ function set(x, y, t) { T[x + ',' + y] = t; x0 = Math.min(x0, x - 1); y0 = Math.min(y0, y - 2); x1 = Math.max(x1, x + 2); y1 = Math.max(y1, y + 2); }
152
+ rooms.forEach(function (q) {
153
+ for (var yy = q.y; yy < q.y + q.h; yy++) for (var xx = q.x; xx < q.x + q.w; xx++) {
154
+ var v = hash(xx, yy);
155
+ set(xx, yy, v < 0.05 ? FLOOR_STONES : v < 0.2 ? FLOOR_SPECK : FLOOR);
156
+ }
157
+ });
158
+ paths.forEach(function (pa) {
159
+ pa.cells.forEach(function (c) { var v = hash(c[0], c[1]); set(c[0], c[1], v < 0.06 ? CORR_RUBBLE : v < 0.12 ? CORR_HOLES : CORRIDOR); });
160
+ });
161
+ paths.forEach(function (pa) { [pa.cells[0], pa.cells[pa.cells.length - 1]].forEach(function (c) { T[c[0] + ',' + c[1]] = DOOR; }); });
162
+
163
+ // Walls, 3/4 view: a brick face where floor is directly south, a cap row above every face, and a cap
164
+ // on any other empty cell touching floor. Caps get a dark outline on each end that is not more wall.
165
+ var W = {}, x, y, dx, dy;
166
+ for (y = y0; y < y1; y++) for (x = x0; x < x1; x++) {
167
+ if (at(x, y) != null) continue;
168
+ var near = false;
169
+ for (dy = -1; dy <= 1; dy++) for (dx = -1; dx <= 1; dx++) near = near || at(x + dx, y + dy) != null;
170
+ if (at(x, y + 1) != null) W[x + ',' + y] = hash(x, y) < 0.08 ? WALL_GRATE : WALL;
171
+ else if (near) W[x + ',' + y] = WALL_TOP;
172
+ }
173
+ function face(x, y) { var t = W[x + ',' + y]; return t === WALL || t === WALL_GRATE; }
174
+ for (y = y0; y < y1; y++) for (x = x0; x < x1; x++) {
175
+ if (at(x, y) == null && !W[x + ',' + y] && (face(x - 1, y + 1) || face(x, y + 1) || face(x + 1, y + 1))) W[x + ',' + y] = 'cap';
176
+ }
177
+ var tiles = [];
178
+ for (y = y0; y < y1; y++) {
179
+ var row = [];
180
+ for (x = x0; x < x1; x++) {
181
+ var t = at(x, y) != null ? at(x, y) : W[x + ',' + y];
182
+ if (t === WALL_TOP || t === 'cap') {
183
+ var l = !!W[(x - 1) + ',' + y], r = !!W[(x + 1) + ',' + y];
184
+ t = l && r ? WALL_TOP : l ? TOP_R : r ? TOP_L : TOP_LR;
185
+ }
186
+ row.push(t == null ? -1 : t);
187
+ }
188
+ tiles.push(row);
189
+ }
190
+ var deco = [];
191
+ rooms.forEach(function (q) { deco.push.apply(deco, decorate(seed, q)); });
192
+ return { w: x1 - x0, h: y1 - y0, ox: x0, oy: y0, tiles: tiles, rooms: rooms, paths: paths, deco: deco };
193
+ }
194
+
195
+ // sheets = { td: Image, ob: Image }; tiles first, then deco in list order (rug under table under candle).
196
+ function render(ctx, d, sheets, scale) {
197
+ var S = 16 * scale;
198
+ function draw(sheet, t, x, y) {
199
+ ctx.drawImage(sheets[sheet], (t % COLS[sheet]) * 16, ((t / COLS[sheet]) | 0) * 16, 16, 16, x * S, y * S, S, S);
200
+ }
201
+ ctx.imageSmoothingEnabled = false;
202
+ d.tiles.forEach(function (row, y) { row.forEach(function (t, x) { if (t >= 0) draw('td', t, x, y); }); });
203
+ d.deco.forEach(function (e) { draw(e.sheet, e.idx, e.x - d.ox, e.y - d.oy); });
204
+ }
205
+
206
+ var Dungeon = { generate: generate, render: render, prng: prng, COLS: COLS, T: {
207
+ FLOOR: FLOOR, CORRIDOR: CORRIDOR, DOOR: DOOR, WALL: WALL, WALL_TOP: WALL_TOP, WALK: WALK,
208
+ BANNER: BANNER, HEARTH_L: HEARTH_L, TABLE: TABLE, CABINET: CABINET, OB_RUG: OB_RUG } };
209
+ if (typeof module !== 'undefined' && module.exports) module.exports = Dungeon;
210
+ root.Dungeon = Dungeon;
211
+ })(typeof window !== 'undefined' ? window : this);
@@ -0,0 +1,63 @@
1
+ /* Pure layout for the timeline (Gantt) view: one row per node in tree order, one bar per node on a shared time axis. Plain ES5;
2
+ * window.Gantt, and module.exports for Node. The viewer draws the result; it applies a minimum bar width in CSS, not here.
3
+ * shortcut: a session with a long idle gap squeezes its bars into the two ends; zoom is the answer for now, gap compression is the upgrade. */
4
+ (function (root) {
5
+ 'use strict';
6
+ var STEPS = [10, 30, 60, 300, 600, 1800, 3600, 7200, 21600, 86400]; // tick spacing in seconds
7
+ var MAXTICKS = 8;
8
+ function num(x) { return typeof x === 'number' && isFinite(x); }
9
+ function clamp(x, lo, hi) { return Math.max(lo, Math.min(hi, x)); }
10
+ function p2(n) { return n < 10 ? '0' + n : '' + n; }
11
+
12
+ function ticks(t0, t1, x) {
13
+ var span = t1 - t0, step = STEPS[STEPS.length - 1];
14
+ for (var i = 0; i < STEPS.length; i++) { if (Math.floor(span / STEPS[i]) + 1 <= MAXTICKS) { step = STEPS[i]; break; } }
15
+ if (Math.floor(span / step) + 1 > MAXTICKS) step = 86400 * Math.ceil(span / 86400 / MAXTICKS); // longer than 8 days: whole days
16
+ var off = -new Date(t0 * 1000).getTimezoneOffset() * 60, out = []; // ticks fall on local clock multiples of step
17
+ for (var t = Math.ceil((t0 + off) / step) * step - off; t <= t1; t += step) {
18
+ var d = new Date(t * 1000);
19
+ out.push({ x: x(t), label: step >= 86400 ? p2(d.getMonth() + 1) + '-' + p2(d.getDate()) : p2(d.getHours()) + ':' + p2(d.getMinutes()) + (step < 60 ? ':' + p2(d.getSeconds()) : '') });
20
+ }
21
+ return out;
22
+ }
23
+
24
+ // nodes: main first, each {id, parent, start, end, running}. t0 / t1 (optional) only widen the range: the caller passes "now" for a live session.
25
+ // Returns {t0, t1, rows: [{id, depth, x, w}], ticks: [{x, label}]}; x and w are fractions of the range, 0..1.
26
+ function layout(nodes, t0, t1) {
27
+ nodes = nodes || [];
28
+ var starts = nodes.map(function (n) { return n.start; }).filter(num), ends = nodes.map(function (n) { return n.end; }).filter(num).concat(starts);
29
+ var lo = Math.min.apply(null, starts.concat(num(t0) ? [t0] : [])), hi = Math.max.apply(null, ends.concat(num(t1) ? [t1] : []));
30
+ if (!isFinite(lo)) lo = isFinite(hi) ? hi : 0;
31
+ if (!isFinite(hi) || hi <= lo) hi = lo + 1;
32
+ var span = hi - lo, fx = function (t) { return clamp((t - lo) / span, 0, 1); };
33
+ var by = {}, kids = {}, rows = [], seen = {};
34
+ nodes.forEach(function (n) { by[n.id] = n; });
35
+ var rootId = nodes.length ? nodes[0].id : null;
36
+ nodes.slice(1).forEach(function (n) { // an unknown (or self) parent hangs the node under main
37
+ var p = by[n.parent] && n.parent !== n.id ? n.parent : rootId;
38
+ (kids[p] = kids[p] || []).push(n);
39
+ });
40
+ function bar(n, depth) {
41
+ var x = 1, w = 0;
42
+ if (num(n.start)) {
43
+ var e = num(n.end) ? n.end : n.running ? hi : n.start;
44
+ x = fx(n.start); w = Math.max(0, fx(e) - x);
45
+ }
46
+ rows.push({ id: n.id, depth: depth, x: x, w: w });
47
+ }
48
+ function walk(n, depth) {
49
+ if (seen[n.id]) return;
50
+ seen[n.id] = 1;
51
+ bar(n, depth);
52
+ (kids[n.id] || []).sort(function (a, b) { return (num(a.start) ? a.start : Infinity) - (num(b.start) ? b.start : Infinity) || 0; })
53
+ .forEach(function (k) { walk(k, depth + 1); });
54
+ }
55
+ if (nodes.length) walk(nodes[0], 0);
56
+ nodes.forEach(function (n) { walk(n, 1); }); // a parent cycle is unreachable from main: still shown once, under it
57
+ return { t0: lo, t1: hi, rows: rows, ticks: ticks(lo, hi, fx) };
58
+ }
59
+
60
+ var Gantt = { layout: layout };
61
+ if (typeof module !== 'undefined' && module.exports) module.exports = Gantt;
62
+ root.Gantt = Gantt;
63
+ })(typeof window !== 'undefined' ? window : this);
@@ -0,0 +1,46 @@
1
+ /* Pure helpers for the lobby list: search, project filter, paging. Plain ES5; window.Lobby, and module.exports for Node.
2
+ * Rows are the lobby rows of index.data.js (no prompt or reply text, so search covers cwd, id, planner and last event only). */
3
+ (function (root) {
4
+ 'use strict';
5
+ function s(x) { return x == null ? '' : String(x); }
6
+ function parts(cwd) { return s(cwd).split('/').filter(Boolean); }
7
+
8
+ // Rows in proj (when set) whose cwd + id + planner + last text contain every space-separated word of q, case-insensitive.
9
+ function filter(rows, q, proj) {
10
+ var words = s(q).toLowerCase().split(/\s+/).filter(Boolean);
11
+ return (rows || []).filter(function (r) {
12
+ if (proj && s(r.cwd) !== proj) return false;
13
+ var hay = [r.cwd, r.id, r.planner, r.last].map(s).join('\n').toLowerCase();
14
+ return words.every(function (w) { return hay.indexOf(w) >= 0; });
15
+ });
16
+ }
17
+
18
+ // [{cwd, name, n}] newest first. name = last path part; two cwds with the same last part get their parent folder added: "api (work)".
19
+ function projects(rows) {
20
+ var by = {}, list = [];
21
+ (rows || []).forEach(function (r) {
22
+ var c = s(r.cwd);
23
+ if (!c) return;
24
+ if (!by[c]) { by[c] = { cwd: c, name: parts(c).pop() || '/', n: 0, t: -Infinity }; list.push(by[c]); }
25
+ by[c].n++;
26
+ by[c].t = Math.max(by[c].t, +r.updated || 0);
27
+ });
28
+ var seen = {};
29
+ list.forEach(function (p) { seen[p.name] = (seen[p.name] || 0) + 1; });
30
+ list.forEach(function (p) { if (seen[p.name] > 1) p.name += ' (' + (parts(p.cwd).slice(-2, -1)[0] || p.cwd) + ')'; });
31
+ list.sort(function (a, b) { return b.t - a.t; }); // stable in modern engines: equal times keep first-seen order
32
+ return list.map(function (p) { return { cwd: p.cwd, name: p.name, n: p.n }; });
33
+ }
34
+
35
+ // One page of list; page is 0-based and clamped, so a list that shrank never leaves you on an empty page.
36
+ function page(list, pg, size) {
37
+ list = list || [];
38
+ size = size > 0 ? Math.floor(size) : 1;
39
+ var pages = Math.max(1, Math.ceil(list.length / size)), p = Math.max(0, Math.min(pages - 1, Math.floor(+pg) || 0));
40
+ return { items: list.slice(p * size, p * size + size), page: p, pages: pages };
41
+ }
42
+
43
+ var Lobby = { filter: filter, projects: projects, page: page };
44
+ if (typeof module !== 'undefined' && module.exports) module.exports = Lobby;
45
+ root.Lobby = Lobby;
46
+ })(typeof window !== 'undefined' ? window : this);