@cactus-agents/games
SDK framework-agnostic para jogos de cassino, categorias, providers, busca, start-game, votos e estatisticas.
Instalacao
pnpm add @cactus-agents/games
Uso recomendado
Use createGamesFromClient quando voce ja estiver usando @cactus-agents/api-client:
import { createApiClient } from "@cactus-agents/api-client";
import { createGamesFromClient } from "@cactus-agents/games";
const client = createApiClient({ baseUrl, tenant, language });
const games = createGamesFromClient(client);
const rows = await games.getPageRows("home");
const base = await games.getBase();
const list = await games.getList({ categories: ["slots"], page: 1, perPage: 24 });
API publica
Exports principais:
createGamesService(fetcher),createGamesFromClient(client)createLegacyGamesService(fetcher, homeConfig),createLegacyGamesFromClient(client, homeConfig)createVotesOrchestrator(...)- Transforms:
transformGame,transformProvider,transformCategory,transformHomeRow,transformBaseData,transformGameListResponse,transformStartGameResponse,transformTopWin,transformLastWin,transformGameTopWinsDl,transformGameHighPayers,transformGameHighPayerEntry,transformGameStatistics,transformGameStatisticsDl,transformGameVote,transformGameVoteCount - Helpers:
filterGames,foldForSearch,orderByStats,getProviderIframeConfig,applyIsSoftSwiss,parseCasinoProviderMessage - Constante:
HIGH_PAYERS_WINDOW_TO_METRIC
Tipos principais:
Game,GameProvider,GameType,GameRange,Provider,CustomCategoryPageSlug,HomeRow,HomeRowType,BaseDataGameListParams,GameListResponse,GameListFacetsStartGamePlatform,StartGameParams,StartGameResponseTopWin,LastWin,GameTopWin,GameHighPayers,GameHighPayersWindowStatsPeriod,GameStatisticsVoteChoice,GameUserVote,GameVoteCount,GameVoteStateGamesService,GamesFetcher
Referencia de exports: front-cactus-core/packages/games/src/index.ts.
:::caution GameVote não existe
O tipo do voto do usuário é GameUserVote ({ casinoGameId: string; choice: VoteChoice }). Existem também GameVoteRaw (shape da API) e GameVoteState ({ userVote, counts }). Nenhum export chamado GameVote.
:::
GamesFetcher
O GamesFetcher e a interface de abstraction de HTTP usada pelo createGamesService. Deve implementar os metodos get, post e delete:
get— usado pela maioria dos endpoints de leiturapost— usado porvote(criar voto)delete— usado porremoveVote(remover voto)
O createGamesFromClient adapta o ApiClient para essa interface automaticamente.
Endpoints do GamesService
| Metodo | Verbo | Endpoint | Observacao |
|---|---|---|---|
getPageRows(page) | GET | /casino-games/page/{page} | Rows curadas de home / cassino / cassino_live |
getHome() | GET | /casino-games/home | @deprecated — use getPageRows("home") |
getBase() | GET | /casino-games/list/base | Categorias + providers |
getList(params?) | GET | /casino-games/list/ | Paginado: categories[], providers[], term, page, per_page |
getDetail(slug) | GET | /casino-games?slug={slug} | Detalhe de um jogo |
startGame(params) | GET | /start-game-v2 | slug, platform, use_demo, currency? — autenticado |
getTopWins() | GET | /bff/games/top-wins-dl | Maiores ganhos recentes |
getGameTopWins(slug) | GET | /bff/games/game-top-wins-dl?slug={slug} | Maiores ganhos de um jogo |
getLastWins() | GET | /bff/games/last-wins | Ultimos ganhos |
getHighPayers() | GET | /bff/games/game-high-payers-dl | Jogos que mais pagam, por janela |
getStats(slug) | GET | /bff/games/statistics?slug={slug} | Stats por periodo |
getStatsDl(slug) | GET | /bff/games/statistics-dl?slug={slug} | Variante -dl das stats |
getVote(casinoGameId) | GET | /casino-game-votes?casinoGameId={id} | Voto do usuario — autenticado |
getVoteCount(gameId) | GET | /casino-game-votes/count/{id} | Contagem likes/dislikes |
vote(casinoGameId, choice) | POST | /casino-game-votes/store/ | Criar voto — autenticado |
removeVote(gameId) | DELETE | /casino-game-votes/destroy/{id} | Remover voto — autenticado |
:::caution vote() não aceita boolean
A assinatura é vote(casinoGameId: string, choice: VoteChoice), com VoteChoice = "liked" | "disliked". O body enviado é { casinoGameId: Number(id), is_like: choice } — apesar do nome is_like, o valor no wire é a string do enum, não um booleano. Passar true/false não type-checka.
:::
Páginas curadas — getPageRows(page)
type PageSlug = "home" | "cassino" | "cassino_live";
getPageRows é o contrato de páginas curadas do modo CASSINO_MODE=api_new: o backoffice monta as rows de cada página e o front só renderiza. getHome() continua exportado e bate em /casino-games/home verbatim (compatibilidade com bases antigas), mas está @deprecated — código novo usa getPageRows("home").
Facets em getList
GameListResponse pode trazer facets:
interface GameListFacets {
providers?: GameListFacetProviderRaw[];
categories?: GameListFacetCategoryRaw[];
}
facets é ausente quando a query não tem filtro — o campo é opcional e o consumidor precisa tratar undefined (não é []).
Helpers de imagem
Os helpers de imagem foram centralizados em @cactus-agents/utils, para que o mesmo contrato sirva para qualquer imagem do projeto:
- Cloudflare
imagedelivery.net - thumbs legadas com
/mobilee/ipad /api/storage/...- imagens externas ou paths locais
import { getImageUrl } from "@cactus-agents/utils";
const logoUrl = getImageUrl(brand.appearance.logo);
:::danger Arte de jogo tem perfil único — não monte ladder próprio
Para game.image no front-web-base, use sempre getGameArtworkProps(image, sizes) de ~/utils/game-artwork (que é um wrapper fino sobre getResponsiveImageProps com a ladder de larguras canônica). Não chame getResponsiveImageProps direto com uma ladder sua e não passe aspectRatio/height: o Cloudflare codifica o transform no path, então uma ladder divergente faz a MESMA arte ser baixada duas vezes. O recorte é CSS (object-cover numa caixa de aspect fixo).
:::
Estatisticas de jogo
transformGameStatistics converte os campos flat da API em periodos organizados:
import { transformGameStatistics } from "@cactus-agents/games";
const stats = transformGameStatistics(raw);
// stats.slug
// stats.last1Hour, stats.last24Hours, stats.last7Days, stats.last15Days, stats.last30Days
// stats.last1Hour.bets, stats.last1Hour.rtp, ...
Os cinco periodos sao last1Hour, last24Hours, last7Days, last15Days, last30Days. Nao existe last5Minutes.
interface StatsPeriod {
bets: number;
wins: number;
players: number;
rtp: number;
houseEdge: number | null;
averageBetPerPlayer: number | null;
averageWin: number | null;
}
:::note Os tres campos nullable
houseEdge, averageBetPerPlayer e averageWin sao number | null — a API pode nao mandar. O front nao calcula derivado (nao faca 1 - rtp): card sem dado deve ser marcado como indisponivel, nao inventado.
:::
Integracao server-side (recomendado para cache)
No front-web-base, o servico e encapsulado por GamesCacheService, que adiciona o pipeline do platform-cache (memory + CF Cache API + KV, com stale-while-revalidate). A fabrica e createGamesCacheService(opts) em ~/services/games.server:
import { createGamesCacheService } from "~/services/games.server";
export async function loader({ context }: Route.LoaderArgs) {
const cf = context?.cloudflare?.env;
const waitUntil = context?.cloudflare?.ctx?.waitUntil.bind(context.cloudflare.ctx);
const gamesCacheService = createGamesCacheService({ cloudflareEnv: cf, waitUntil });
const base = await gamesCacheService.getBase();
return { base };
}
A fabrica tambem resolve o CASSINO_MODE da brand e escolhe qual service usar (api_new / legacy / mixed) — o loader nao decide isso.
Para start-game (autenticado), a chamada vai direto na API sem cache:
import { createGamesFromClient } from "@cactus-agents/games";
const client = createClient(env, { request });
const games = createGamesFromClient(client);
const result = await games.startGame({ slug, platform: "WEB", useDemo: false });
Notas de transform
transformGameconvertedemo: 0|1para boolean e normalizaprovider,casino_game_type,rangetransformProviderconvertegames(count) paragameCounttransformCategorymapeia campos 1:1 incluindoicontransformHomeRowclassificatypecomowidget,custom-categoryoucustom-sectiontransformGameListResponseconverte paginacao snake_case para camelCasetransformStartGameResponseextraigameUrlde multiplos formatos possiveis da API (SoftSwiss vs padrao)
Legacy Service
Funcoes para integrar com o endpoint legado de jogos:
createLegacyGamesService(fetcher, homeConfig)— cria service usandoGamesFetcherdiretamentecreateLegacyGamesFromClient(client, homeConfig)— cria service a partir deApiClient
O legacy service usa /casino-games/filter em vez de /casino-games/list. O parametro homeConfig e do tipo LegacyHomeRowConfig[], onde cada item define uma row da home com um dos tipos:
| Tipo | Descricao |
|---|---|
"games-api" | Row populada via filtro na API de jogos |
"providers-api" | Row de providers via API |
"games-fixed" | Row com jogos fixos (slugs hardcoded) |
"widget" | Row de widget customizado |
Provider Config
Configuracao de iframe para providers de jogos:
getProviderIframeConfig(providerSlug, gameUrl?)
Retorna ProviderIframeConfig para o provider especificado:
allow— permissions do iframe (ex.:autoplay,fullscreen)allowFullscreen— booleanlaunchMode— modo de lancamento:"iframe"|"softswiss-sdk"|"srcdoc"
applyIsSoftSwiss(config, isSoftSwiss)
Sobrescreve o launchMode da config quando o jogo e SoftSwiss.
parseCasinoProviderMessage(providerSlug, raw)
Faz parse de postMessage recebidas de iframes de jogos. Retorna acao interpretada por provider:
- liveg24: mensagem
"closeGame"→ acaoexit - amusnet: detecta acoes
exitereload
import { parseCasinoProviderMessage } from "@cactus-agents/games";
window.addEventListener("message", (event) => {
const action = parseCasinoProviderMessage("liveg24", event.data);
if (action === "exit") {
// fechar iframe do jogo
}
});
Votos — createVotesOrchestrator(service)
Encapsula a sequência de votação (paridade com o legado: DELETE → POST → refetch), para o consumidor não reimplementar a ordem:
import { createVotesOrchestrator } from "@cactus-agents/games";
const votes = createVotesOrchestrator(gamesService);
const state = await votes.getState(gameId, { authed: isLoggedIn });
const next = await votes.submit(gameId, "liked");
const cleared = await votes.remove(gameId);
getState(id, { authed })—authed: falsepula o request de voto do usuário (carga de página pública). Erros degetVoteCountpropagam aqui, para o caller distinguir "o BFF devolveu zeros" de "a chamada falhou".submit/remove— o refetch pós-mutação é catch-guarded: a mutação já deu certo, então falha no refetch não quebra o fluxo.
Retorna sempre GameVoteState ({ userVote, counts }).
Ordenação por stats — orderByStats(games, statsBySlug, orderBy)
Ordena Game[] por uma métrica de GameStatistics, indexada por slug. Jogos sem stats vão para o fim preservando a ordem original.
import { orderByStats } from "@cactus-agents/games";
const ordered = orderByStats(games, statsBySlug, category.orderBy);
HIGH_PAYERS_WINDOW_TO_METRIC mapeia a janela do endpoint de high-payers para o tipo de métrica:
{ "1hour": "last_minutes", "24hours": "paid_today",
"7days": "paid_week", "15days": "paid_week", "30days": "paid_month" }
Filtro in-memory
foldForSearch(s)
Normalização usada pela busca: remove diacríticos (NFD + strip) e passa para lowercase. Exportada para que buscas locais em subsets do catálogo (página de categoria, de provider, listagens restritas) usem exatamente a mesma normalização da busca global.
foldForSearch("Ração & Café"); // "racao & cafe"
filterGames(games, params)
Filtra um array de Game[] em memoria (client-side). Util para buscas e filtros sem nova chamada a API.
Params:
| Param | Tipo | Descricao |
|---|---|---|
term | string? | Busca por nome do jogo |
categories | string[]? | Filtro por categorias |
providers | string[]? | Filtro por providers |
tags | string[]? | Filtro por tags |
page | number? | Pagina atual |
perPage | number? | Itens por pagina |
Retorno: GameFilterResult
interface GameFilterResult {
data: Game[];
total: number;
currentPage: number;
lastPage: number;
}
import { filterGames } from "@cactus-agents/games";
const result = filterGames(allGames, {
term: "sweet",
categories: ["slots"],
page: 1,
perPage: 20,
});
// result.data — jogos filtrados da pagina
// result.total — total de resultados
// result.lastPage — ultima pagina disponivel