@k4la/library 1.0.0
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.
- package/data/asahotak.json +1142 -0
- package/data/family100.json +73589 -0
- package/data/susunkata.json +3008 -0
- package/data/tebakbendera.json +346 -0
- package/data/tebakgambar.json +6002 -0
- package/data/tebakkata.json +1512 -0
- package/data/tebakkimia.json +502 -0
- package/index.js +29 -0
- package/lib/blackjack.js +219 -0
- package/lib/converter.js +105 -0
- package/lib/exif.js +205 -0
- package/lib/function.js +13 -0
- package/lib/game.js +58 -0
- package/lib/tictactoe.js +164 -0
- package/lib/ulartangga.js +181 -0
- package/lib/uno.js +250 -0
- package/lib/uploader.js +136 -0
- package/lib/werewolf.js +1208 -0
- package/package.json +23 -0
package/lib/tictactoe.js
ADDED
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file lib/utils/tictactoe.js
|
|
3
|
+
* @description Utility functions for managing Tic Tac Toe game logic
|
|
4
|
+
* @author K4lameety
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const jimp = require('jimp')
|
|
8
|
+
|
|
9
|
+
const gameData = {}
|
|
10
|
+
|
|
11
|
+
const createSession = (id) => {
|
|
12
|
+
const grid = [0, 0, 0, 0, 0, 0, 0, 0, 0]
|
|
13
|
+
|
|
14
|
+
gameData[id] = {
|
|
15
|
+
id: id,
|
|
16
|
+
status: 'waiting',
|
|
17
|
+
players: [],
|
|
18
|
+
grid: grid,
|
|
19
|
+
turn: 0,
|
|
20
|
+
winner: null,
|
|
21
|
+
winningLine: []
|
|
22
|
+
}
|
|
23
|
+
return gameData[id]
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const getSession = (id) => gameData[id] || false
|
|
27
|
+
const deleteSession = (id) => delete gameData[id]
|
|
28
|
+
|
|
29
|
+
const userJoin = (id, sender, name) => {
|
|
30
|
+
let session = getSession(id)
|
|
31
|
+
if (!session) session = createSession(id)
|
|
32
|
+
if (session.status === 'playing') return 'is_playing'
|
|
33
|
+
if (session.players.some(p => p.id === sender)) return 'already_joined'
|
|
34
|
+
if (session.players.length >= 2) return 'room_full'
|
|
35
|
+
|
|
36
|
+
let marker = session.players.length === 0 ? 'X' : 'O'
|
|
37
|
+
session.players.push({ id: sender, name: name, marker: marker })
|
|
38
|
+
|
|
39
|
+
if (session.players.length === 2) {
|
|
40
|
+
session.status = 'playing'
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return 'success'
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const fillBoard = (id, sender, index) => {
|
|
47
|
+
let session = getSession(id)
|
|
48
|
+
if (!session || session.status !== 'playing') return { status: 'error', msg: 'Invalid session' }
|
|
49
|
+
|
|
50
|
+
let playerIdx = session.players.findIndex(p => p.id === sender)
|
|
51
|
+
if (playerIdx !== session.turn) return { status: 'error', msg: 'Not your turn!' }
|
|
52
|
+
|
|
53
|
+
let idx = index - 1
|
|
54
|
+
if (idx < 0 || idx > 8) return { status: 'error', msg: 'Position 1-9' }
|
|
55
|
+
if (session.grid[idx] !== 0) return { status: 'error', msg: 'Box already filled' }
|
|
56
|
+
|
|
57
|
+
session.grid[idx] = session.players[playerIdx].marker
|
|
58
|
+
|
|
59
|
+
let win = checkWin(session.grid)
|
|
60
|
+
if (win) {
|
|
61
|
+
session.winner = session.players[playerIdx]
|
|
62
|
+
session.winningLine = win.line
|
|
63
|
+
return { status: 'win', winner: session.winner, line: win.line }
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (session.grid.every(x => x !== 0)) {
|
|
67
|
+
return { status: 'draw' }
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
session.turn = session.turn === 0 ? 1 : 0
|
|
71
|
+
|
|
72
|
+
return { status: 'success', next: session.players[session.turn] }
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const checkWin = (grid) => {
|
|
76
|
+
const wins = [
|
|
77
|
+
[0, 1, 2], [3, 4, 5], [6, 7, 8],
|
|
78
|
+
[0, 3, 6], [1, 4, 7], [2, 5, 8],
|
|
79
|
+
[0, 4, 8], [2, 4, 6]
|
|
80
|
+
]
|
|
81
|
+
|
|
82
|
+
for (let line of wins) {
|
|
83
|
+
if (grid[line[0]] !== 0 &&
|
|
84
|
+
grid[line[0]] === grid[line[1]] &&
|
|
85
|
+
grid[line[1]] === grid[line[2]]) {
|
|
86
|
+
return { winner: grid[line[0]], line: line }
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return null
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const canvas = async (id, sock) => {
|
|
93
|
+
let session = getSession(id)
|
|
94
|
+
if (!session) return null
|
|
95
|
+
|
|
96
|
+
let bg = new jimp(500, 600, 0xFFFFFFFF)
|
|
97
|
+
|
|
98
|
+
let font = await jimp.loadFont(jimp.FONT_SANS_32_BLACK)
|
|
99
|
+
let fontSmall = await jimp.loadFont(jimp.FONT_SANS_16_BLACK)
|
|
100
|
+
|
|
101
|
+
bg.scan(166, 50, 5, 400, (x, y, idx) => { bg.bitmap.data[idx] = 0; bg.bitmap.data[idx+1] = 0; bg.bitmap.data[idx+2] = 0; bg.bitmap.data[idx+3] = 255 })
|
|
102
|
+
bg.scan(332, 50, 5, 400, (x, y, idx) => { bg.bitmap.data[idx] = 0; bg.bitmap.data[idx+1] = 0; bg.bitmap.data[idx+2] = 0; bg.bitmap.data[idx+3] = 255 })
|
|
103
|
+
|
|
104
|
+
bg.scan(50, 183, 400, 5, (x, y, idx) => { bg.bitmap.data[idx] = 0; bg.bitmap.data[idx+1] = 0; bg.bitmap.data[idx+2] = 0; bg.bitmap.data[idx+3] = 255 })
|
|
105
|
+
bg.scan(50, 316, 400, 5, (x, y, idx) => { bg.bitmap.data[idx] = 0; bg.bitmap.data[idx+1] = 0; bg.bitmap.data[idx+2] = 0; bg.bitmap.data[idx+3] = 255 })
|
|
106
|
+
|
|
107
|
+
const coords = [
|
|
108
|
+
[50, 50], [216, 50], [382, 50],
|
|
109
|
+
[50, 183], [216, 183], [382, 183],
|
|
110
|
+
[50, 316], [216, 316], [382, 316]
|
|
111
|
+
]
|
|
112
|
+
|
|
113
|
+
for (let i = 0; i < 9; i++) {
|
|
114
|
+
if (session.grid[i] === 0) {
|
|
115
|
+
bg.print(fontSmall, coords[i][0] + 5, coords[i][1] + 5, `${i+1}`)
|
|
116
|
+
} else {
|
|
117
|
+
let marker = session.grid[i]
|
|
118
|
+
let player = session.players.find(p => p.marker === marker)
|
|
119
|
+
|
|
120
|
+
try {
|
|
121
|
+
let ppUrl = await sock.profilePictureUrl(player.id, 'image').catch(() => 'https://telegra.ph/file/24fa902ead26340f3df2c.png')
|
|
122
|
+
let pp = await jimp.read(ppUrl)
|
|
123
|
+
|
|
124
|
+
pp.resize(100, 100).circle()
|
|
125
|
+
bg.composite(pp, coords[i][0] + 30, coords[i][1] + 15)
|
|
126
|
+
} catch (e) {
|
|
127
|
+
bg.print(font, coords[i][0] + 60, coords[i][1] + 40, marker)
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (session.winningLine && session.winningLine.length > 0) {
|
|
133
|
+
let redBox = new jimp(160, 130, 0xFF000055)
|
|
134
|
+
|
|
135
|
+
for (let idx of session.winningLine) {
|
|
136
|
+
let x = coords[idx][0]
|
|
137
|
+
let y = coords[idx][1]
|
|
138
|
+
bg.composite(redBox, x, y)
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
let p1Name = session.players[0] ? session.players[0].name : "-"
|
|
143
|
+
let p2Name = session.players[1] ? session.players[1].name : "-"
|
|
144
|
+
|
|
145
|
+
bg.print(fontSmall, 20, 520, `X: ${p1Name}`)
|
|
146
|
+
bg.print(fontSmall, 20, 550, `O: ${p2Name}`)
|
|
147
|
+
|
|
148
|
+
let turnName = session.status === 'playing'
|
|
149
|
+
? (session.winner ? `WINNER: ${session.winner.name}` : `Turn: ${session.players[session.turn].name}`)
|
|
150
|
+
: "Waiting for Players..."
|
|
151
|
+
|
|
152
|
+
bg.print(font, 80, 470, turnName)
|
|
153
|
+
|
|
154
|
+
return await bg.getBufferAsync(jimp.MIME_JPEG)
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
module.exports = {
|
|
158
|
+
createSession,
|
|
159
|
+
userJoin,
|
|
160
|
+
fillBoard,
|
|
161
|
+
getSession,
|
|
162
|
+
deleteSession,
|
|
163
|
+
canvas
|
|
164
|
+
}
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file lib/utils/ulartangga.js
|
|
3
|
+
* @description Snake and Ladder game utilities
|
|
4
|
+
* @author K4lameety
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const jimp = require('jimp')
|
|
8
|
+
|
|
9
|
+
const gameData = {}
|
|
10
|
+
|
|
11
|
+
const BOARD_URL = "https://raw.githubusercontent.com/K4lameety/game-assets/main/snake-and-ladder/board.png"
|
|
12
|
+
|
|
13
|
+
const MAP_CONFIG = {
|
|
14
|
+
4: 14,
|
|
15
|
+
9: 31,
|
|
16
|
+
20: 38,
|
|
17
|
+
28: 84,
|
|
18
|
+
40: 59,
|
|
19
|
+
51: 67,
|
|
20
|
+
63: 81,
|
|
21
|
+
71: 91,
|
|
22
|
+
|
|
23
|
+
17: 7,
|
|
24
|
+
54: 34,
|
|
25
|
+
62: 19,
|
|
26
|
+
64: 60,
|
|
27
|
+
87: 24,
|
|
28
|
+
93: 73,
|
|
29
|
+
95: 75,
|
|
30
|
+
99: 78
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const createSession = (id) => {
|
|
34
|
+
gameData[id] = {
|
|
35
|
+
id: id,
|
|
36
|
+
status: 'waiting',
|
|
37
|
+
players: [],
|
|
38
|
+
turn: 0
|
|
39
|
+
}
|
|
40
|
+
return gameData[id]
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const getSession = (id) => gameData[id] || false
|
|
44
|
+
const deleteSession = (id) => delete gameData[id]
|
|
45
|
+
|
|
46
|
+
const userJoin = (id, sender, name) => {
|
|
47
|
+
let session = getSession(id)
|
|
48
|
+
if (!session) session = createSession(id)
|
|
49
|
+
if (session.status === 'playing') return 'is_playing'
|
|
50
|
+
if (session.players.some(p => p.id === sender)) return 'already_joined'
|
|
51
|
+
if (session.players.length >= 4) return 'room_full'
|
|
52
|
+
|
|
53
|
+
const colors = [0xFF0000FF, 0x00FF00FF, 0x0000FFFF, 0xFFFF00FF]
|
|
54
|
+
let color = colors[session.players.length]
|
|
55
|
+
|
|
56
|
+
session.players.push({ id: sender, name: name, color: color, pos: 1 })
|
|
57
|
+
return 'success'
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const startGame = (id) => {
|
|
61
|
+
let session = getSession(id)
|
|
62
|
+
if (!session || session.players.length < 2) return false
|
|
63
|
+
session.status = 'playing'
|
|
64
|
+
return true
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const rollDice = (id, sender) => {
|
|
68
|
+
let session = getSession(id)
|
|
69
|
+
if (!session || session.status !== 'playing') return { status: 'error', msg: 'Session invalid' }
|
|
70
|
+
|
|
71
|
+
let playerIdx = session.players.findIndex(p => p.id === sender)
|
|
72
|
+
if (playerIdx !== session.turn) return { status: 'error', msg: 'Not your turn!' }
|
|
73
|
+
|
|
74
|
+
let player = session.players[playerIdx]
|
|
75
|
+
|
|
76
|
+
let dice = Math.floor(Math.random() * 6) + 1
|
|
77
|
+
let oldPos = player.pos
|
|
78
|
+
let newPos = oldPos + dice
|
|
79
|
+
|
|
80
|
+
if (newPos > 100) {
|
|
81
|
+
newPos = 100 - (newPos - 100)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
let message = `Dice: ${dice}. Moving to ${newPos}.`
|
|
85
|
+
|
|
86
|
+
if (MAP_CONFIG[newPos]) {
|
|
87
|
+
let dest = MAP_CONFIG[newPos]
|
|
88
|
+
if (dest > newPos) message += `\nClimbed LADDER! To ${dest}`
|
|
89
|
+
else message += `\nBitten by SNAKE! Down to ${dest}`
|
|
90
|
+
newPos = dest
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
player.pos = newPos
|
|
94
|
+
|
|
95
|
+
if (newPos === 100) {
|
|
96
|
+
return { status: 'win', winner: player }
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
session.turn = (session.turn + 1) % session.players.length
|
|
100
|
+
|
|
101
|
+
return { status: 'success', player, dice, message, next: session.players[session.turn] }
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const canvas = async (id, sock) => {
|
|
105
|
+
let session = getSession(id)
|
|
106
|
+
if (!session) return null
|
|
107
|
+
|
|
108
|
+
let bg
|
|
109
|
+
try {
|
|
110
|
+
bg = await jimp.read(BOARD_URL)
|
|
111
|
+
bg.resize(600, 600)
|
|
112
|
+
} catch (e) {
|
|
113
|
+
console.log('Failed to load board image from URL')
|
|
114
|
+
bg = new jimp(600, 600, 0xFFFFFFFF)
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
let layout = new jimp(600, 700, 0xFFFFFFFF)
|
|
118
|
+
layout.composite(bg, 0, 0)
|
|
119
|
+
|
|
120
|
+
let font = await jimp.loadFont(jimp.FONT_SANS_16_BLACK)
|
|
121
|
+
let fontBig = await jimp.loadFont(jimp.FONT_SANS_32_BLACK)
|
|
122
|
+
|
|
123
|
+
for (let p of session.players) {
|
|
124
|
+
let { x, y } = getCoord(p.pos)
|
|
125
|
+
|
|
126
|
+
try {
|
|
127
|
+
let ppUrl = await sock.profilePictureUrl(p.id, 'image').catch(() => 'https://telegra.ph/file/24fa902ead26340f3df2c.png')
|
|
128
|
+
let pp = await jimp.read(ppUrl)
|
|
129
|
+
pp.resize(40, 40).circle()
|
|
130
|
+
|
|
131
|
+
let border = new jimp(44, 44, p.color)
|
|
132
|
+
border.circle()
|
|
133
|
+
border.composite(pp, 2, 2)
|
|
134
|
+
|
|
135
|
+
let stackIndex = session.players.filter(pl => pl.pos === p.pos && session.players.indexOf(pl) < session.players.indexOf(p)).length
|
|
136
|
+
let offsetX = stackIndex * 10
|
|
137
|
+
let offsetY = stackIndex * 5
|
|
138
|
+
|
|
139
|
+
layout.composite(border, x + 8 + offsetX, y + 8 + offsetY)
|
|
140
|
+
|
|
141
|
+
} catch (e) { }
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
let turnName = session.status === 'playing'
|
|
145
|
+
? `Turn: ${session.players[session.turn].name}`
|
|
146
|
+
: "Waiting for Players..."
|
|
147
|
+
|
|
148
|
+
let statusPos = session.players.map(p => `${p.name.slice(0,5)}: ${p.pos}`).join(' | ')
|
|
149
|
+
|
|
150
|
+
layout.print(fontBig, 20, 610, turnName)
|
|
151
|
+
layout.print(font, 20, 660, statusPos)
|
|
152
|
+
|
|
153
|
+
return await layout.getBufferAsync(jimp.MIME_JPEG)
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const getCoord = (pos) => {
|
|
157
|
+
if (pos < 1) pos = 1
|
|
158
|
+
if (pos > 100) pos = 100
|
|
159
|
+
|
|
160
|
+
let row = Math.floor((pos - 1) / 10)
|
|
161
|
+
let col = (pos - 1) % 10
|
|
162
|
+
|
|
163
|
+
if (row % 2 !== 0) {
|
|
164
|
+
col = 9 - col
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
let x = col * 60
|
|
168
|
+
let y = (9 - row) * 60
|
|
169
|
+
|
|
170
|
+
return { x, y }
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
module.exports = {
|
|
174
|
+
createSession,
|
|
175
|
+
userJoin,
|
|
176
|
+
startGame,
|
|
177
|
+
rollDice,
|
|
178
|
+
getSession,
|
|
179
|
+
deleteSession,
|
|
180
|
+
canvas
|
|
181
|
+
}
|
package/lib/uno.js
ADDED
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file lib/utils/uno.js
|
|
3
|
+
* @description Utility functions for managing Uno game logic
|
|
4
|
+
* @author K4lameety
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const fs = require('fs')
|
|
8
|
+
const jimp = require('jimp')
|
|
9
|
+
|
|
10
|
+
const GITHUB_BASE_URL = "https://raw.githubusercontent.com/K4lameety/game-assets/main/uno/"
|
|
11
|
+
|
|
12
|
+
const gameData = {}
|
|
13
|
+
|
|
14
|
+
const shuffle = (array) => {
|
|
15
|
+
for (let i = array.length - 1; i > 0; i--) {
|
|
16
|
+
const j = Math.floor(Math.random() * (i + 1));
|
|
17
|
+
[array[i], array[j]] = [array[j], array[i]]
|
|
18
|
+
}
|
|
19
|
+
return array
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const mod = (n, m) => ((n % m) + m) % m
|
|
23
|
+
|
|
24
|
+
const generateDeck = () => {
|
|
25
|
+
const colors = ['red', 'green', 'blue', 'yellow']
|
|
26
|
+
const numbers = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9']
|
|
27
|
+
const actions = ['skip', 'reverse', 'draw2']
|
|
28
|
+
const wilds = ['wild', 'wild4']
|
|
29
|
+
|
|
30
|
+
let deck = []
|
|
31
|
+
|
|
32
|
+
colors.forEach(color => {
|
|
33
|
+
numbers.forEach(num => {
|
|
34
|
+
let count = (num === '0') ? 1 : 2
|
|
35
|
+
for (let i = 0; i < count; i++) {
|
|
36
|
+
deck.push({
|
|
37
|
+
color: color,
|
|
38
|
+
value: num,
|
|
39
|
+
id: `${num}_card_${color}`
|
|
40
|
+
})
|
|
41
|
+
}
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
actions.forEach(act => {
|
|
45
|
+
for (let i = 0; i < 2; i++) {
|
|
46
|
+
let idName = ''
|
|
47
|
+
if (act === 'skip') idName = `special_block_card_${color}`
|
|
48
|
+
if (act === 'reverse') idName = `special_revert_card_${color}`
|
|
49
|
+
if (act === 'draw2') idName = `special_2_card_${color}`
|
|
50
|
+
|
|
51
|
+
deck.push({
|
|
52
|
+
color: color,
|
|
53
|
+
value: act,
|
|
54
|
+
id: idName
|
|
55
|
+
})
|
|
56
|
+
}
|
|
57
|
+
})
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
wilds.forEach(wild => {
|
|
61
|
+
for (let i = 0; i < 4; i++) {
|
|
62
|
+
let idName = ''
|
|
63
|
+
if (wild === 'wild') idName = 'special_colorful_card'
|
|
64
|
+
if (wild === 'wild4') idName = 'special_4_card'
|
|
65
|
+
|
|
66
|
+
deck.push({
|
|
67
|
+
color: 'black',
|
|
68
|
+
value: wild,
|
|
69
|
+
id: idName
|
|
70
|
+
})
|
|
71
|
+
}
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
return shuffle(deck)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const createSession = (id) => {
|
|
78
|
+
gameData[id] = {
|
|
79
|
+
id: id,
|
|
80
|
+
status: 'waiting',
|
|
81
|
+
players: [],
|
|
82
|
+
deck: [],
|
|
83
|
+
pile: {},
|
|
84
|
+
turn: 0,
|
|
85
|
+
direction: 1,
|
|
86
|
+
currentColor: ''
|
|
87
|
+
}
|
|
88
|
+
return gameData[id]
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const getSession = (id) => gameData[id] || false
|
|
92
|
+
const deleteSession = (id) => delete gameData[id]
|
|
93
|
+
|
|
94
|
+
const userJoin = (id, sender, name) => {
|
|
95
|
+
let session = getSession(id)
|
|
96
|
+
if (!session) session = createSession(id)
|
|
97
|
+
if (session.status === 'playing') return 'is_playing'
|
|
98
|
+
if (session.players.some(p => p.id === sender)) return 'already_joined'
|
|
99
|
+
session.players.push({ id: sender, name: name, hand: [] })
|
|
100
|
+
return 'success'
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const startGame = (id) => {
|
|
104
|
+
let session = getSession(id)
|
|
105
|
+
if (!session || session.players.length < 2) return false
|
|
106
|
+
|
|
107
|
+
session.status = 'playing'
|
|
108
|
+
session.deck = generateDeck()
|
|
109
|
+
session.players.forEach(p => { p.hand = session.deck.splice(0, 7) })
|
|
110
|
+
|
|
111
|
+
let firstCard
|
|
112
|
+
do {
|
|
113
|
+
session.deck = shuffle(session.deck)
|
|
114
|
+
firstCard = session.deck.pop()
|
|
115
|
+
} while (firstCard.color === 'black')
|
|
116
|
+
|
|
117
|
+
session.pile = firstCard
|
|
118
|
+
session.currentColor = firstCard.color
|
|
119
|
+
|
|
120
|
+
return session
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const playCard = (id, sender, index, wildColor = null) => {
|
|
124
|
+
let session = getSession(id)
|
|
125
|
+
if (!session) return { status: 'error', msg: 'No session' }
|
|
126
|
+
|
|
127
|
+
let player = session.players[session.turn]
|
|
128
|
+
if (player.id !== sender) return { status: 'error', msg: 'Not your turn' }
|
|
129
|
+
if (!player.hand[index]) return { status: 'error', msg: 'Card not found' }
|
|
130
|
+
|
|
131
|
+
let card = player.hand[index]
|
|
132
|
+
let top = session.pile
|
|
133
|
+
|
|
134
|
+
let valid = false
|
|
135
|
+
if (card.color === session.currentColor) valid = true
|
|
136
|
+
if (card.value === top.value) valid = true
|
|
137
|
+
if (card.color === 'black') valid = true
|
|
138
|
+
|
|
139
|
+
if (!valid) return { status: 'error', msg: 'Card does not match' }
|
|
140
|
+
|
|
141
|
+
player.hand.splice(index, 1)
|
|
142
|
+
session.pile = card
|
|
143
|
+
|
|
144
|
+
if (card.color === 'black') {
|
|
145
|
+
if (wildColor) wildColor = wildColor.toLowerCase()
|
|
146
|
+
if (!wildColor || !['red', 'green', 'blue', 'yellow'].includes(wildColor)) {
|
|
147
|
+
player.hand.push(card)
|
|
148
|
+
return { status: 'error', msg: 'Please specify the color! Example: .uno play 1 red' }
|
|
149
|
+
}
|
|
150
|
+
session.currentColor = wildColor
|
|
151
|
+
} else {
|
|
152
|
+
session.currentColor = card.color
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
if (player.hand.length === 0) return { status: 'win', winner: player }
|
|
156
|
+
|
|
157
|
+
let skip = 0
|
|
158
|
+
|
|
159
|
+
if (card.value === 'reverse') {
|
|
160
|
+
if (session.players.length === 2) skip = 1
|
|
161
|
+
else session.direction *= -1
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (card.value === 'skip') skip = 1
|
|
165
|
+
|
|
166
|
+
if (card.value === 'draw2') {
|
|
167
|
+
skip = 1
|
|
168
|
+
let victimIndex = mod(session.turn + session.direction, session.players.length)
|
|
169
|
+
session.players[victimIndex].hand.push(...session.deck.splice(0, 2))
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if (card.value === 'wild4') {
|
|
173
|
+
skip = 1
|
|
174
|
+
let victimIndex = mod(session.turn + session.direction, session.players.length)
|
|
175
|
+
session.players[victimIndex].hand.push(...session.deck.splice(0, 4))
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
session.turn = mod(session.turn + (session.direction * (1 + skip)), session.players.length)
|
|
179
|
+
if (session.deck.length < 5) session.deck = [...session.deck, ...generateDeck()]
|
|
180
|
+
|
|
181
|
+
return { status: 'success', next: session.players[session.turn] }
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const drawCard = (id, sender) => {
|
|
185
|
+
let session = getSession(id)
|
|
186
|
+
let player = session.players[session.turn]
|
|
187
|
+
if (player.id !== sender) return { status: 'error' }
|
|
188
|
+
|
|
189
|
+
let card = session.deck.pop()
|
|
190
|
+
player.hand.push(card)
|
|
191
|
+
session.turn = mod(session.turn + session.direction, session.players.length)
|
|
192
|
+
|
|
193
|
+
return { status: 'success', card: card, next: session.players[session.turn] }
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const canvas = async (id, sender) => {
|
|
197
|
+
let session = getSession(id)
|
|
198
|
+
if (!session) return null
|
|
199
|
+
let player = session.players.find(p => p.id === sender)
|
|
200
|
+
if (!player) return null
|
|
201
|
+
|
|
202
|
+
let bg = new jimp(1200, 800, 0x222222FF)
|
|
203
|
+
let font = await jimp.loadFont(jimp.FONT_SANS_32_WHITE)
|
|
204
|
+
|
|
205
|
+
let pileUrl = `${GITHUB_BASE_URL}${session.pile.id}.png`
|
|
206
|
+
|
|
207
|
+
try {
|
|
208
|
+
let pileImg = await jimp.read(pileUrl)
|
|
209
|
+
pileImg.resize(180, 260)
|
|
210
|
+
bg.composite(pileImg, 510, 200)
|
|
211
|
+
} catch (e) {
|
|
212
|
+
bg.print(font, 510, 250, session.pile.value)
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
bg.print(font, 50, 50, `Color: ${session.currentColor.toUpperCase()}`)
|
|
216
|
+
bg.print(font, 50, 100, `Player: ${session.players[session.turn].name}`)
|
|
217
|
+
bg.print(font, 900, 50, `Direction: ${session.direction === 1 ? '>>' : '<<'}`)
|
|
218
|
+
|
|
219
|
+
let x = 50
|
|
220
|
+
let y = 500
|
|
221
|
+
|
|
222
|
+
for (let i = 0; i < player.hand.length; i++) {
|
|
223
|
+
let card = player.hand[i]
|
|
224
|
+
let cardUrl = `${GITHUB_BASE_URL}${card.id}.png`
|
|
225
|
+
|
|
226
|
+
try {
|
|
227
|
+
let cardImg = await jimp.read(cardUrl)
|
|
228
|
+
cardImg.resize(120, 175)
|
|
229
|
+
bg.composite(cardImg, x, y)
|
|
230
|
+
bg.print(font, x + 45, y - 40, `${i + 1}`)
|
|
231
|
+
|
|
232
|
+
x += 100
|
|
233
|
+
if (x > 1050) { x = 50; y += 190 }
|
|
234
|
+
} catch (e) {
|
|
235
|
+
bg.print(font, x, y, card.value)
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
return await bg.getBufferAsync(jimp.MIME_JPEG)
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
module.exports = {
|
|
243
|
+
userJoin,
|
|
244
|
+
startGame,
|
|
245
|
+
playCard,
|
|
246
|
+
drawCard,
|
|
247
|
+
getSession,
|
|
248
|
+
deleteSession,
|
|
249
|
+
canvas
|
|
250
|
+
}
|