git-slot-machine 2.3.2 → 2.4.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/package.json +6 -1
- package/.claude/release.md +0 -74
- package/CHANGELOG.md +0 -250
- package/dist/animation/slotMachine.d.ts.map +0 -1
- package/dist/animation/slotMachine.js.map +0 -1
- package/dist/api.d.ts.map +0 -1
- package/dist/api.js.map +0 -1
- package/dist/balance.d.ts.map +0 -1
- package/dist/balance.js.map +0 -1
- package/dist/commands/auth.d.ts.map +0 -1
- package/dist/commands/auth.js.map +0 -1
- package/dist/commands/balance.d.ts.map +0 -1
- package/dist/commands/balance.js.map +0 -1
- package/dist/commands/config.d.ts.map +0 -1
- package/dist/commands/config.js.map +0 -1
- package/dist/commands/init.d.ts.map +0 -1
- package/dist/commands/init.js.map +0 -1
- package/dist/commands/play.d.ts.map +0 -1
- package/dist/commands/play.js.map +0 -1
- package/dist/commands/spin.d.ts.map +0 -1
- package/dist/commands/spin.js.map +0 -1
- package/dist/commands/sync.d.ts.map +0 -1
- package/dist/commands/sync.js.map +0 -1
- package/dist/commands/test.d.ts.map +0 -1
- package/dist/commands/test.js.map +0 -1
- package/dist/config.d.ts.map +0 -1
- package/dist/config.js.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/patterns.d.ts.map +0 -1
- package/dist/patterns.js.map +0 -1
- package/dist/secrets.d.ts.map +0 -1
- package/dist/secrets.js.map +0 -1
- package/dist/templates/post-commit.d.ts.map +0 -1
- package/dist/templates/post-commit.js.map +0 -1
- package/dist/utils/amendDetector.d.ts.map +0 -1
- package/dist/utils/amendDetector.js.map +0 -1
- package/dist/utils/fetch-polyfill.d.ts.map +0 -1
- package/dist/utils/fetch-polyfill.js.map +0 -1
- package/dist/utils/git.d.ts.map +0 -1
- package/dist/utils/git.js.map +0 -1
- package/jest.config.js +0 -12
- package/src/animation/slotMachine.ts +0 -164
- package/src/api.ts +0 -207
- package/src/balance.ts +0 -118
- package/src/commands/auth.ts +0 -92
- package/src/commands/balance.ts +0 -28
- package/src/commands/config.ts +0 -59
- package/src/commands/init.ts +0 -259
- package/src/commands/play.ts +0 -196
- package/src/commands/spin.ts +0 -17
- package/src/commands/sync.ts +0 -49
- package/src/commands/test.ts +0 -19
- package/src/config.ts +0 -189
- package/src/index.ts +0 -136
- package/src/patterns.test.ts +0 -44
- package/src/patterns.ts +0 -313
- package/src/secrets.ts +0 -44
- package/src/templates/post-commit.ts +0 -15
- package/src/utils/amendDetector.ts +0 -74
- package/src/utils/fetch-polyfill.ts +0 -13
- package/src/utils/git.ts +0 -88
- package/test.txt +0 -2
- package/tsconfig.json +0 -21
package/src/config.ts
DELETED
|
@@ -1,189 +0,0 @@
|
|
|
1
|
-
import fs from 'fs';
|
|
2
|
-
import path from 'path';
|
|
3
|
-
import os from 'os';
|
|
4
|
-
import { execSync } from 'child_process';
|
|
5
|
-
|
|
6
|
-
interface Config {
|
|
7
|
-
githubUsername?: string;
|
|
8
|
-
playAsUsername?: string; // Per-repo override: play as this username instead
|
|
9
|
-
apiUrl?: string;
|
|
10
|
-
apiToken?: string;
|
|
11
|
-
syncEnabled?: boolean;
|
|
12
|
-
privateRepo?: boolean;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
// Get repo-specific config path
|
|
16
|
-
function getRepoConfigPath(): string {
|
|
17
|
-
return path.join(process.cwd(), '.git', 'slot-machine-config.json');
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
// Get global config path
|
|
21
|
-
function getGlobalConfigPath(): string {
|
|
22
|
-
const homeDir = os.homedir();
|
|
23
|
-
const configDir = path.join(homeDir, '.git-slot-machine');
|
|
24
|
-
|
|
25
|
-
// Ensure config directory exists
|
|
26
|
-
if (!fs.existsSync(configDir)) {
|
|
27
|
-
fs.mkdirSync(configDir, { recursive: true });
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
return path.join(configDir, 'config.json');
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
// Get merged config (repo-specific overrides global)
|
|
34
|
-
export function getConfig(): Config {
|
|
35
|
-
const globalConfig = getGlobalConfig();
|
|
36
|
-
const repoConfig = getRepoConfig();
|
|
37
|
-
|
|
38
|
-
return { ...globalConfig, ...repoConfig };
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
// Get only repo-specific config
|
|
42
|
-
export function getRepoConfig(): Config {
|
|
43
|
-
const configPath = getRepoConfigPath();
|
|
44
|
-
|
|
45
|
-
if (!fs.existsSync(configPath)) {
|
|
46
|
-
return {};
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
try {
|
|
50
|
-
const content = fs.readFileSync(configPath, 'utf-8');
|
|
51
|
-
return JSON.parse(content);
|
|
52
|
-
} catch {
|
|
53
|
-
return {};
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
// Get only global config
|
|
58
|
-
export function getGlobalConfig(): Config {
|
|
59
|
-
const configPath = getGlobalConfigPath();
|
|
60
|
-
|
|
61
|
-
if (!fs.existsSync(configPath)) {
|
|
62
|
-
return {};
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
try {
|
|
66
|
-
const content = fs.readFileSync(configPath, 'utf-8');
|
|
67
|
-
return JSON.parse(content);
|
|
68
|
-
} catch {
|
|
69
|
-
return {};
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
// Save repo-specific config
|
|
74
|
-
export function saveRepoConfig(config: Config): void {
|
|
75
|
-
const configPath = getRepoConfigPath();
|
|
76
|
-
fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
// Save global config
|
|
80
|
-
export function saveGlobalConfig(config: Config): void {
|
|
81
|
-
const configPath = getGlobalConfigPath();
|
|
82
|
-
fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
export function getGitHubUsername(): string | null {
|
|
86
|
-
const config = getConfig();
|
|
87
|
-
// Check for per-repo override first, then fall back to global username
|
|
88
|
-
return config.playAsUsername || config.githubUsername || null;
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
export function setGitHubUsername(username: string): void {
|
|
92
|
-
const config = getGlobalConfig();
|
|
93
|
-
config.githubUsername = username;
|
|
94
|
-
saveGlobalConfig(config);
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
export function getApiUrl(): string {
|
|
98
|
-
const config = getConfig();
|
|
99
|
-
return config.apiUrl || process.env.GIT_SLOT_MACHINE_API_URL || 'https://gitslotmachine.com/api';
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
export function setApiUrl(url: string): void {
|
|
103
|
-
const config = getGlobalConfig();
|
|
104
|
-
config.apiUrl = url;
|
|
105
|
-
saveGlobalConfig(config);
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
export function getApiToken(): string | null {
|
|
109
|
-
const config = getConfig();
|
|
110
|
-
return config.apiToken || null;
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
export function setApiToken(token: string): void {
|
|
114
|
-
const config = getGlobalConfig();
|
|
115
|
-
config.apiToken = token;
|
|
116
|
-
saveGlobalConfig(config);
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
export function clearApiToken(): void {
|
|
120
|
-
const config = getGlobalConfig();
|
|
121
|
-
delete config.apiToken;
|
|
122
|
-
saveGlobalConfig(config);
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
export function isSyncEnabled(): boolean {
|
|
126
|
-
const config = getConfig();
|
|
127
|
-
return config.syncEnabled !== false; // Default to true
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
export function setSyncEnabled(enabled: boolean): void {
|
|
131
|
-
const config = getGlobalConfig();
|
|
132
|
-
config.syncEnabled = enabled;
|
|
133
|
-
saveGlobalConfig(config);
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
export function isPrivateRepo(): boolean {
|
|
137
|
-
const config = getRepoConfig();
|
|
138
|
-
return config.privateRepo === true;
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
export function setPrivateRepo(isPrivate: boolean): void {
|
|
142
|
-
const config = getRepoConfig();
|
|
143
|
-
config.privateRepo = isPrivate;
|
|
144
|
-
saveRepoConfig(config);
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
export function setPlayAsUsername(username: string): void {
|
|
148
|
-
const config = getRepoConfig();
|
|
149
|
-
config.playAsUsername = username;
|
|
150
|
-
saveRepoConfig(config);
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
export function getPlayAsUsername(): string | null {
|
|
154
|
-
const config = getRepoConfig();
|
|
155
|
-
return config.playAsUsername || null;
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
export function getRepoInfo(): { owner: string; name: string; url: string } | null {
|
|
159
|
-
try {
|
|
160
|
-
const remoteUrl = execSync('git config --get remote.origin.url', { encoding: 'utf-8' }).trim();
|
|
161
|
-
|
|
162
|
-
// Parse GitHub URL (supports both HTTPS and SSH)
|
|
163
|
-
const match = remoteUrl.match(/github\.com[:/](.+?)\/(.+?)(\.git)?$/);
|
|
164
|
-
|
|
165
|
-
if (match) {
|
|
166
|
-
const owner = match[1];
|
|
167
|
-
const name = match[2];
|
|
168
|
-
|
|
169
|
-
// If privacy mode is enabled, return obfuscated info
|
|
170
|
-
if (isPrivateRepo()) {
|
|
171
|
-
return {
|
|
172
|
-
owner: 'private',
|
|
173
|
-
name: 'private',
|
|
174
|
-
url: 'private',
|
|
175
|
-
};
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
return {
|
|
179
|
-
owner,
|
|
180
|
-
name,
|
|
181
|
-
url: `https://github.com/${owner}/${name}`,
|
|
182
|
-
};
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
return null;
|
|
186
|
-
} catch {
|
|
187
|
-
return null;
|
|
188
|
-
}
|
|
189
|
-
}
|
package/src/index.ts
DELETED
|
@@ -1,136 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
import { Command } from 'commander';
|
|
4
|
-
import chalk from 'chalk';
|
|
5
|
-
import { playCommand } from './commands/play.js';
|
|
6
|
-
import { spinCommand } from './commands/spin.js';
|
|
7
|
-
import { initCommand } from './commands/init.js';
|
|
8
|
-
import { balanceCommand } from './commands/balance.js';
|
|
9
|
-
import { testCommand } from './commands/test.js';
|
|
10
|
-
import { authLoginCommand, authLogoutCommand, authStatusCommand } from './commands/auth.js';
|
|
11
|
-
import { syncCommand } from './commands/sync.js';
|
|
12
|
-
import { configGetCommand, configSetCommand } from './commands/config.js';
|
|
13
|
-
import { createRequire } from 'module';
|
|
14
|
-
|
|
15
|
-
const require = createRequire(import.meta.url);
|
|
16
|
-
const { version } = require('../package.json');
|
|
17
|
-
|
|
18
|
-
const program = new Command();
|
|
19
|
-
|
|
20
|
-
program
|
|
21
|
-
.name('git-slot-machine')
|
|
22
|
-
.description('Git commit hash slot machine')
|
|
23
|
-
.version(version, '-v, --version', 'Output the current version');
|
|
24
|
-
|
|
25
|
-
program
|
|
26
|
-
.command('play')
|
|
27
|
-
.description('Play the slot machine with a git hash')
|
|
28
|
-
.argument('<hash>', '7-character git commit hash')
|
|
29
|
-
.option('-s, --small', 'Single line output')
|
|
30
|
-
.action(async (hash: string, options: any) => {
|
|
31
|
-
await playCommand(hash, options);
|
|
32
|
-
});
|
|
33
|
-
|
|
34
|
-
program
|
|
35
|
-
.command('spin')
|
|
36
|
-
.description('Play with the current git commit hash')
|
|
37
|
-
.option('-s, --small', 'Single line output')
|
|
38
|
-
.action(async (options: any) => {
|
|
39
|
-
await spinCommand(options);
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
program
|
|
43
|
-
.command('init')
|
|
44
|
-
.description('Install post-commit hook in current repository')
|
|
45
|
-
.action(async () => {
|
|
46
|
-
await initCommand();
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
program
|
|
50
|
-
.command('balance')
|
|
51
|
-
.description('Show current repository balance and stats')
|
|
52
|
-
.action(balanceCommand);
|
|
53
|
-
|
|
54
|
-
program
|
|
55
|
-
.command('test')
|
|
56
|
-
.description('Play with a random 7-character hash')
|
|
57
|
-
.option('-s, --small', 'Single line output')
|
|
58
|
-
.action(async (options: any) => {
|
|
59
|
-
await testCommand(options);
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
// Auth commands (top-level)
|
|
63
|
-
program
|
|
64
|
-
.command('login')
|
|
65
|
-
.description('Login with GitHub username to join the leaderboard')
|
|
66
|
-
.argument('<github-username>', 'Your GitHub username')
|
|
67
|
-
.action(async (githubUsername: string) => {
|
|
68
|
-
await authLoginCommand(githubUsername);
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
program
|
|
72
|
-
.command('logout')
|
|
73
|
-
.description('Logout and clear authentication')
|
|
74
|
-
.action(async () => {
|
|
75
|
-
await authLogoutCommand();
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
program
|
|
79
|
-
.command('status')
|
|
80
|
-
.description('Show authentication and sync status')
|
|
81
|
-
.action(async () => {
|
|
82
|
-
await authStatusCommand();
|
|
83
|
-
});
|
|
84
|
-
|
|
85
|
-
// Sync commands
|
|
86
|
-
program
|
|
87
|
-
.command('sync')
|
|
88
|
-
.description('Sync balance with API')
|
|
89
|
-
.action(async () => {
|
|
90
|
-
await syncCommand();
|
|
91
|
-
});
|
|
92
|
-
|
|
93
|
-
program
|
|
94
|
-
.command('sync:enable')
|
|
95
|
-
.description('Enable automatic API sync')
|
|
96
|
-
.action(async () => {
|
|
97
|
-
await configSetCommand('sync-enabled', 'true');
|
|
98
|
-
});
|
|
99
|
-
|
|
100
|
-
program
|
|
101
|
-
.command('sync:disable')
|
|
102
|
-
.description('Disable automatic API sync')
|
|
103
|
-
.action(async () => {
|
|
104
|
-
await configSetCommand('sync-enabled', 'false');
|
|
105
|
-
});
|
|
106
|
-
|
|
107
|
-
// Username commands
|
|
108
|
-
program
|
|
109
|
-
.command('username:set')
|
|
110
|
-
.description('Set GitHub username')
|
|
111
|
-
.argument('<username>', 'Your GitHub username')
|
|
112
|
-
.action(async (username: string) => {
|
|
113
|
-
const { setGitHubUsername } = await import('./config.js');
|
|
114
|
-
setGitHubUsername(username);
|
|
115
|
-
console.log(chalk.green(`GitHub username set to: ${username}`));
|
|
116
|
-
});
|
|
117
|
-
|
|
118
|
-
// Config commands (advanced - hidden from main help)
|
|
119
|
-
program
|
|
120
|
-
.command('config:get', { hidden: true })
|
|
121
|
-
.description('Get configuration value (advanced)')
|
|
122
|
-
.argument('<key>', 'Configuration key (api-url, sync-enabled, all)')
|
|
123
|
-
.action(async (key: string) => {
|
|
124
|
-
await configGetCommand(key);
|
|
125
|
-
});
|
|
126
|
-
|
|
127
|
-
program
|
|
128
|
-
.command('config:set', { hidden: true })
|
|
129
|
-
.description('Set configuration value (advanced)')
|
|
130
|
-
.argument('<key>', 'Configuration key (api-url, sync-enabled)')
|
|
131
|
-
.argument('<value>', 'Configuration value')
|
|
132
|
-
.action(async (key: string, value: string) => {
|
|
133
|
-
await configSetCommand(key, value);
|
|
134
|
-
});
|
|
135
|
-
|
|
136
|
-
program.parse();
|
package/src/patterns.test.ts
DELETED
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
import { detectPattern, PatternType } from './patterns.js';
|
|
2
|
-
|
|
3
|
-
describe('Pattern Detection', () => {
|
|
4
|
-
it('detects all same character', () => {
|
|
5
|
-
const result = detectPattern('aaaaaaa');
|
|
6
|
-
expect(result.type).toBe(PatternType.ALL_SAME);
|
|
7
|
-
expect(result.name).toBe('JACKPOT');
|
|
8
|
-
});
|
|
9
|
-
|
|
10
|
-
it('detects 4 of a kind', () => {
|
|
11
|
-
const result = detectPattern('aaaa123');
|
|
12
|
-
expect(result.type).toBe(PatternType.FOUR_OF_KIND);
|
|
13
|
-
expect(result.payout).toBeGreaterThan(0);
|
|
14
|
-
});
|
|
15
|
-
|
|
16
|
-
it('detects fullest house (4-3)', () => {
|
|
17
|
-
const result = detectPattern('aaaabbb');
|
|
18
|
-
expect(result.type).toBe(PatternType.FULLEST_HOUSE);
|
|
19
|
-
});
|
|
20
|
-
|
|
21
|
-
it('detects three pair', () => {
|
|
22
|
-
const result = detectPattern('aabbcc1');
|
|
23
|
-
expect(result.type).toBe(PatternType.THREE_PAIR);
|
|
24
|
-
});
|
|
25
|
-
|
|
26
|
-
it('detects one pair', () => {
|
|
27
|
-
const result = detectPattern('aa12345');
|
|
28
|
-
expect(result.type).toBe(PatternType.ONE_PAIR);
|
|
29
|
-
});
|
|
30
|
-
|
|
31
|
-
it('detects no win', () => {
|
|
32
|
-
const result = detectPattern('1234567');
|
|
33
|
-
expect(result.type).toBe(PatternType.NO_WIN);
|
|
34
|
-
expect(result.payout).toBe(0);
|
|
35
|
-
});
|
|
36
|
-
|
|
37
|
-
it('validates hash length', () => {
|
|
38
|
-
expect(() => detectPattern('abc')).toThrow('Hash must be 7 characters');
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
it('validates hex characters', () => {
|
|
42
|
-
expect(() => detectPattern('gggggg1')).toThrow('Hash must contain only hex characters');
|
|
43
|
-
});
|
|
44
|
-
});
|
package/src/patterns.ts
DELETED
|
@@ -1,313 +0,0 @@
|
|
|
1
|
-
export enum PatternType {
|
|
2
|
-
SECRET = 'SECRET', // Hidden combos
|
|
3
|
-
LUCKY_SEVENS = 'LUCKY_SEVENS', // Secret: 7777777
|
|
4
|
-
ALL_SAME = 'ALL_SAME',
|
|
5
|
-
SIX_OF_KIND = 'SIX_OF_KIND',
|
|
6
|
-
STRAIGHT_7 = 'STRAIGHT_7',
|
|
7
|
-
FULLEST_HOUSE = 'FULLEST_HOUSE', // 4-3
|
|
8
|
-
FIVE_OF_KIND = 'FIVE_OF_KIND',
|
|
9
|
-
STRAIGHT_6 = 'STRAIGHT_6',
|
|
10
|
-
FOUR_OF_KIND = 'FOUR_OF_KIND',
|
|
11
|
-
ALL_LETTERS = 'ALL_LETTERS',
|
|
12
|
-
STRAIGHT_5 = 'STRAIGHT_5',
|
|
13
|
-
THREE_OF_KIND_PLUS_THREE = 'THREE_OF_KIND_PLUS_THREE', // 3-3-1
|
|
14
|
-
FULLER_HOUSE = 'FULLER_HOUSE', // 3-2-2
|
|
15
|
-
FULL_HOUSE = 'FULL_HOUSE', // 3-2-1-1
|
|
16
|
-
THREE_PAIR = 'THREE_PAIR',
|
|
17
|
-
THREE_OF_KIND = 'THREE_OF_KIND',
|
|
18
|
-
TWO_PAIR = 'TWO_PAIR',
|
|
19
|
-
ALL_NUMBERS = 'ALL_NUMBERS',
|
|
20
|
-
ONE_PAIR = 'ONE_PAIR',
|
|
21
|
-
NO_WIN = 'NO_WIN'
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
export interface PatternResult {
|
|
25
|
-
type: PatternType;
|
|
26
|
-
name: string;
|
|
27
|
-
payout: number;
|
|
28
|
-
description: string;
|
|
29
|
-
highlightIndices: number[]; // Indices of characters to highlight
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
const PAYOUTS: Record<PatternType, { name: string; payout: number; description: string }> = {
|
|
33
|
-
[PatternType.SECRET]: { name: 'SECRET', payout: 0, description: '???' }, // Placeholder, actual values from secrets.ts
|
|
34
|
-
[PatternType.LUCKY_SEVENS]: { name: 'LUCKY SEVENS', payout: 1000000, description: 'The ultimate jackpot: 7777777' },
|
|
35
|
-
[PatternType.ALL_SAME]: { name: 'JACKPOT', payout: 100000, description: 'All same character' },
|
|
36
|
-
[PatternType.STRAIGHT_7]: { name: 'LUCKY SEVEN', payout: 50000, description: 'Seven sequential hex digits' },
|
|
37
|
-
[PatternType.STRAIGHT_6]: { name: 'BIG STRAIGHT', payout: 25000, description: 'Six sequential hex digits' },
|
|
38
|
-
[PatternType.SIX_OF_KIND]: { name: 'HEXTET', payout: 10000, description: 'Six of a kind' },
|
|
39
|
-
[PatternType.FULLEST_HOUSE]: { name: 'FULLEST HOUSE', payout: 5000, description: '4 + 3 of a kind' },
|
|
40
|
-
[PatternType.STRAIGHT_5]: { name: 'STRAIGHT', payout: 2500, description: 'Five sequential hex digits' },
|
|
41
|
-
[PatternType.FIVE_OF_KIND]: { name: 'FIVE OF A KIND', payout: 2000, description: 'Five of a kind' },
|
|
42
|
-
[PatternType.THREE_OF_KIND_PLUS_THREE]: { name: 'DOUBLE TRIPLE', payout: 1000, description: 'Two three of a kinds' },
|
|
43
|
-
[PatternType.THREE_PAIR]: { name: 'THREE PAIR', payout: 500, description: 'Three consecutive pairs' },
|
|
44
|
-
[PatternType.FULLER_HOUSE]: { name: 'FULLER HOUSE', payout: 400, description: '3 + 2 + 2 of a kind' },
|
|
45
|
-
[PatternType.FULL_HOUSE]: { name: 'FULL HOUSE', payout: 50, description: '3 + 2 of a kind' },
|
|
46
|
-
[PatternType.ALL_LETTERS]: { name: 'ALPHABET SOUP', payout: 250, description: 'Only letters (a-f)' },
|
|
47
|
-
[PatternType.FOUR_OF_KIND]: { name: 'FOUR OF A KIND', payout: 200, description: 'Four of a kind' },
|
|
48
|
-
[PatternType.THREE_OF_KIND]: { name: 'THREE OF A KIND', payout: 25, description: 'Three of a kind' },
|
|
49
|
-
[PatternType.ALL_NUMBERS]: { name: 'ALL NUMBERS', payout: 50, description: 'Only numbers (0-9)' },
|
|
50
|
-
[PatternType.TWO_PAIR]: { name: 'TWO PAIR', payout: 25, description: 'Two consecutive pairs' },
|
|
51
|
-
[PatternType.ONE_PAIR]: { name: 'ONE PAIR', payout: 10, description: 'One consecutive pair' },
|
|
52
|
-
[PatternType.NO_WIN]: { name: 'NO WIN', payout: 0, description: 'No winning pattern' }
|
|
53
|
-
};
|
|
54
|
-
|
|
55
|
-
function countCharacters(hash: string): Map<string, number> {
|
|
56
|
-
const counts = new Map<string, number>();
|
|
57
|
-
for (const char of hash) {
|
|
58
|
-
counts.set(char, (counts.get(char) || 0) + 1);
|
|
59
|
-
}
|
|
60
|
-
return counts;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
function getCountDistribution(counts: Map<string, number>): number[] {
|
|
64
|
-
return Array.from(counts.values()).sort((a, b) => b - a);
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
function hasSequentialRun(hash: string, length: number): boolean {
|
|
68
|
-
const hexValues = '0123456789abcdef';
|
|
69
|
-
|
|
70
|
-
for (let i = 0; i <= hash.length - length; i++) {
|
|
71
|
-
const substring = hash.substring(i, i + length);
|
|
72
|
-
|
|
73
|
-
// Check ascending
|
|
74
|
-
let isAscending = true;
|
|
75
|
-
for (let j = 0; j < substring.length - 1; j++) {
|
|
76
|
-
const currentIndex = hexValues.indexOf(substring[j]);
|
|
77
|
-
const nextIndex = hexValues.indexOf(substring[j + 1]);
|
|
78
|
-
if (nextIndex !== currentIndex + 1) {
|
|
79
|
-
isAscending = false;
|
|
80
|
-
break;
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
if (isAscending) return true;
|
|
84
|
-
|
|
85
|
-
// Check descending
|
|
86
|
-
let isDescending = true;
|
|
87
|
-
for (let j = 0; j < substring.length - 1; j++) {
|
|
88
|
-
const currentIndex = hexValues.indexOf(substring[j]);
|
|
89
|
-
const nextIndex = hexValues.indexOf(substring[j + 1]);
|
|
90
|
-
if (nextIndex !== currentIndex - 1) {
|
|
91
|
-
isDescending = false;
|
|
92
|
-
break;
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
if (isDescending) return true;
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
return false;
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
function isAllLetters(hash: string): boolean {
|
|
102
|
-
return /^[a-f]+$/i.test(hash);
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
function isAllNumbers(hash: string): boolean {
|
|
106
|
-
return /^[0-9]+$/.test(hash);
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
function countConsecutivePairs(hash: string): number {
|
|
110
|
-
// Count pairs of consecutive identical characters (e.g., "33" or "bb")
|
|
111
|
-
let pairCount = 0;
|
|
112
|
-
let i = 0;
|
|
113
|
-
|
|
114
|
-
while (i < hash.length - 1) {
|
|
115
|
-
if (hash[i] === hash[i + 1]) {
|
|
116
|
-
pairCount++;
|
|
117
|
-
i += 2; // Skip both characters of the pair
|
|
118
|
-
} else {
|
|
119
|
-
i++;
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
return pairCount;
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
function getHighlightIndices(hash: string, type: PatternType): number[] {
|
|
127
|
-
const lowerHash = hash.toLowerCase();
|
|
128
|
-
|
|
129
|
-
// For straights, highlight all characters in the sequential run
|
|
130
|
-
if (type === PatternType.STRAIGHT_7 || type === PatternType.STRAIGHT_6 || type === PatternType.STRAIGHT_5) {
|
|
131
|
-
const length = type === PatternType.STRAIGHT_7 ? 7 : type === PatternType.STRAIGHT_6 ? 6 : 5;
|
|
132
|
-
const hexValues = '0123456789abcdef';
|
|
133
|
-
|
|
134
|
-
for (let i = 0; i <= lowerHash.length - length; i++) {
|
|
135
|
-
const substring = lowerHash.substring(i, i + length);
|
|
136
|
-
let isSequential = true;
|
|
137
|
-
|
|
138
|
-
// Check ascending
|
|
139
|
-
for (let j = 0; j < substring.length - 1; j++) {
|
|
140
|
-
const currentIndex = hexValues.indexOf(substring[j]);
|
|
141
|
-
const nextIndex = hexValues.indexOf(substring[j + 1]);
|
|
142
|
-
if (nextIndex !== currentIndex + 1 && nextIndex !== currentIndex - 1) {
|
|
143
|
-
isSequential = false;
|
|
144
|
-
break;
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
if (isSequential) {
|
|
149
|
-
return Array.from({ length }, (_, idx) => i + idx);
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
// For all letters, all numbers, all same, or lucky sevens - highlight everything
|
|
155
|
-
if (type === PatternType.ALL_LETTERS || type === PatternType.ALL_NUMBERS ||
|
|
156
|
-
type === PatternType.ALL_SAME || type === PatternType.LUCKY_SEVENS) {
|
|
157
|
-
return [0, 1, 2, 3, 4, 5, 6];
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
// For frequency-based patterns, highlight characters that appear in the pattern
|
|
161
|
-
const counts = countCharacters(lowerHash);
|
|
162
|
-
const indices: number[] = [];
|
|
163
|
-
|
|
164
|
-
if (type === PatternType.SIX_OF_KIND || type === PatternType.FIVE_OF_KIND ||
|
|
165
|
-
type === PatternType.FOUR_OF_KIND || type === PatternType.THREE_OF_KIND) {
|
|
166
|
-
// Find the character with the highest count and highlight all occurrences
|
|
167
|
-
const targetCount = type === PatternType.SIX_OF_KIND ? 6 :
|
|
168
|
-
type === PatternType.FIVE_OF_KIND ? 5 :
|
|
169
|
-
type === PatternType.FOUR_OF_KIND ? 4 : 3;
|
|
170
|
-
|
|
171
|
-
for (const [char, count] of counts.entries()) {
|
|
172
|
-
if (count === targetCount) {
|
|
173
|
-
for (let i = 0; i < lowerHash.length; i++) {
|
|
174
|
-
if (lowerHash[i] === char) {
|
|
175
|
-
indices.push(i);
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
break;
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
else if (type === PatternType.FULLEST_HOUSE || type === PatternType.FULLER_HOUSE ||
|
|
183
|
-
type === PatternType.FULL_HOUSE || type === PatternType.THREE_OF_KIND_PLUS_THREE) {
|
|
184
|
-
// Highlight all paired/tripled characters
|
|
185
|
-
for (const [char, count] of counts.entries()) {
|
|
186
|
-
if (count >= 2) {
|
|
187
|
-
for (let i = 0; i < lowerHash.length; i++) {
|
|
188
|
-
if (lowerHash[i] === char) {
|
|
189
|
-
indices.push(i);
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
}
|
|
195
|
-
else if (type === PatternType.THREE_PAIR || type === PatternType.TWO_PAIR || type === PatternType.ONE_PAIR) {
|
|
196
|
-
// Highlight consecutive pairs only (e.g., "aa" and "bb" in "aa1bb2c")
|
|
197
|
-
let i = 0;
|
|
198
|
-
while (i < lowerHash.length - 1) {
|
|
199
|
-
if (lowerHash[i] === lowerHash[i + 1]) {
|
|
200
|
-
indices.push(i);
|
|
201
|
-
indices.push(i + 1);
|
|
202
|
-
i += 2; // Skip both characters of the pair
|
|
203
|
-
} else {
|
|
204
|
-
i++;
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
return indices.sort((a, b) => a - b);
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
export function detectPattern(hash: string): PatternResult {
|
|
213
|
-
// Validate input
|
|
214
|
-
if (hash.length !== 7) {
|
|
215
|
-
throw new Error('Hash must be 7 characters');
|
|
216
|
-
}
|
|
217
|
-
if (!/^[0-9a-f]+$/i.test(hash)) {
|
|
218
|
-
throw new Error('Hash must contain only hex characters');
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
const lowerHash = hash.toLowerCase();
|
|
222
|
-
const counts = countCharacters(lowerHash);
|
|
223
|
-
const distribution = getCountDistribution(counts);
|
|
224
|
-
|
|
225
|
-
// Detect pattern - check in order of rarity/value
|
|
226
|
-
let type: PatternType;
|
|
227
|
-
|
|
228
|
-
// Check for the secret ultimate jackpot: 7777777
|
|
229
|
-
if (lowerHash === '7777777') {
|
|
230
|
-
type = PatternType.LUCKY_SEVENS;
|
|
231
|
-
}
|
|
232
|
-
// Check for all same first (highest value)
|
|
233
|
-
else if (distribution[0] === 7) {
|
|
234
|
-
type = PatternType.ALL_SAME;
|
|
235
|
-
}
|
|
236
|
-
// Check for 6 of a kind
|
|
237
|
-
else if (distribution[0] === 6) {
|
|
238
|
-
type = PatternType.SIX_OF_KIND;
|
|
239
|
-
}
|
|
240
|
-
// Check for straight 7 (very rare)
|
|
241
|
-
else if (hasSequentialRun(lowerHash, 7)) {
|
|
242
|
-
type = PatternType.STRAIGHT_7;
|
|
243
|
-
}
|
|
244
|
-
// Check for fullest house (4-3)
|
|
245
|
-
else if (distribution[0] === 4 && distribution[1] === 3) {
|
|
246
|
-
type = PatternType.FULLEST_HOUSE;
|
|
247
|
-
}
|
|
248
|
-
// Check for 5 of a kind
|
|
249
|
-
else if (distribution[0] === 5) {
|
|
250
|
-
type = PatternType.FIVE_OF_KIND;
|
|
251
|
-
}
|
|
252
|
-
// Check for straight 6
|
|
253
|
-
else if (hasSequentialRun(lowerHash, 6)) {
|
|
254
|
-
type = PatternType.STRAIGHT_6;
|
|
255
|
-
}
|
|
256
|
-
// Check for 4 of a kind
|
|
257
|
-
else if (distribution[0] === 4) {
|
|
258
|
-
type = PatternType.FOUR_OF_KIND;
|
|
259
|
-
}
|
|
260
|
-
// Check for all letters
|
|
261
|
-
else if (isAllLetters(lowerHash)) {
|
|
262
|
-
type = PatternType.ALL_LETTERS;
|
|
263
|
-
}
|
|
264
|
-
// Check for straight 5
|
|
265
|
-
else if (hasSequentialRun(lowerHash, 5)) {
|
|
266
|
-
type = PatternType.STRAIGHT_5;
|
|
267
|
-
}
|
|
268
|
-
// Check for double triple (3-3-1)
|
|
269
|
-
else if (distribution[0] === 3 && distribution[1] === 3) {
|
|
270
|
-
type = PatternType.THREE_OF_KIND_PLUS_THREE;
|
|
271
|
-
}
|
|
272
|
-
// Check for fuller house (3-2-2)
|
|
273
|
-
else if (distribution[0] === 3 && distribution[1] === 2 && distribution[2] === 2) {
|
|
274
|
-
type = PatternType.FULLER_HOUSE;
|
|
275
|
-
}
|
|
276
|
-
// Check for full house (3-2-1-1)
|
|
277
|
-
else if (distribution[0] === 3 && distribution[1] === 2) {
|
|
278
|
-
type = PatternType.FULL_HOUSE;
|
|
279
|
-
}
|
|
280
|
-
// Check for 3 of a kind
|
|
281
|
-
else if (distribution[0] === 3) {
|
|
282
|
-
type = PatternType.THREE_OF_KIND;
|
|
283
|
-
}
|
|
284
|
-
// Check for three consecutive pairs
|
|
285
|
-
else if (countConsecutivePairs(lowerHash) === 3) {
|
|
286
|
-
type = PatternType.THREE_PAIR;
|
|
287
|
-
}
|
|
288
|
-
// Check for two consecutive pairs
|
|
289
|
-
else if (countConsecutivePairs(lowerHash) === 2) {
|
|
290
|
-
type = PatternType.TWO_PAIR;
|
|
291
|
-
}
|
|
292
|
-
// Check for all numbers
|
|
293
|
-
else if (isAllNumbers(lowerHash)) {
|
|
294
|
-
type = PatternType.ALL_NUMBERS;
|
|
295
|
-
}
|
|
296
|
-
// Check for one consecutive pair
|
|
297
|
-
else if (countConsecutivePairs(lowerHash) === 1) {
|
|
298
|
-
type = PatternType.ONE_PAIR;
|
|
299
|
-
}
|
|
300
|
-
// Everything else is no win
|
|
301
|
-
else {
|
|
302
|
-
type = PatternType.NO_WIN;
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
const config = PAYOUTS[type];
|
|
306
|
-
return {
|
|
307
|
-
type,
|
|
308
|
-
name: config.name,
|
|
309
|
-
payout: config.payout,
|
|
310
|
-
description: config.description,
|
|
311
|
-
highlightIndices: getHighlightIndices(hash, type)
|
|
312
|
-
};
|
|
313
|
-
}
|