Commit 6ce8f229 authored by Mahmoud Aglan's avatar Mahmoud Aglan

fix(chess): bot games crashed on resign and finish

A top-level const in api/game.php was declared below the dispatch, so it was
not defined yet when finishing a bot game called chessBotTooShort(): every
bot game ended in "Undefined constant", was never closed and never paid.
Declared above the dispatch now.

Also the applied follow-up migration: account deletion no longer fails for
code creators and reviewers, legacy group codes are unique by canonical form,
tournament codes register through tournament_register (no drafts, window and
fee honoured), the table keeps its game order, the test ad is paused.
Co-Authored-By: 's avatarClaude Opus 5.5 <noreply@anthropic.com>
parent f4eec81f
......@@ -12,6 +12,11 @@ require_once __DIR__ . '/../includes/chess.php';
require_once __DIR__ . '/../includes/tournament-engine.php';
require_once __DIR__ . '/../includes/ghost.php';
/** Seconds a game against a bot must last before it pays (see finaliseMatch).
* Declared before the dispatch below: a top-level const further down the file
* is not defined yet when the handler runs. */
const CHESS_BOT_MIN_PAID_SECONDS = 60;
$token = requireAuth();
$userId = getUserId($token);
$input = getInput();
......@@ -51,8 +56,6 @@ switch ($action) {
jsonError('Invalid action');
}
/** Seconds a game against a bot must last before it pays (see finaliseMatch). */
const CHESS_BOT_MIN_PAID_SECONDS = 60;
function chessBotTooShort(?string $startedAt): bool {
$ts = strtotime((string) $startedAt);
......
-- Follow-ups after the org/redeem layer and the game-controls work.
-- 1. Deleting an account failed (500) for anyone who had created a code,
-- reviewed an application or referred someone: five keys to profiles had
-- no ON DELETE action. They now let go (SET NULL).
-- 2. Legacy group codes are hex and the resolver matches on the canonical
-- form, where B folds to 8: two groups could share a code. A unique index
-- on the canonical form stops it, and new codes no longer contain B.
-- 3. A tournament code registered players into drafts, skipped the
-- registration window and never charged the entry fee. It now goes
-- through tournament_register unless the code waives the fee.
-- 4. Data: keep the table's game order as players know it (chess, ludo,
-- domino, backgammon) and pause the test ad, which would otherwise start
-- showing on the chess table now that ads are wired.
BEGIN;
SET LOCAL lock_timeout = '5s';
-- 1. Keys that blocked account deletion
ALTER TABLE public.org_membership_applications DROP CONSTRAINT IF EXISTS org_membership_applications_reviewed_by_fkey,
ADD CONSTRAINT org_membership_applications_reviewed_by_fkey FOREIGN KEY (reviewed_by) REFERENCES public.profiles(id) ON DELETE SET NULL;
ALTER TABLE public.org_referrals DROP CONSTRAINT IF EXISTS org_referrals_referrer_id_fkey,
ADD CONSTRAINT org_referrals_referrer_id_fkey FOREIGN KEY (referrer_id) REFERENCES public.profiles(id) ON DELETE SET NULL;
ALTER TABLE public.org_referrals DROP CONSTRAINT IF EXISTS org_referrals_referred_id_fkey,
ADD CONSTRAINT org_referrals_referred_id_fkey FOREIGN KEY (referred_id) REFERENCES public.profiles(id) ON DELETE SET NULL;
ALTER TABLE public.redeem_codes DROP CONSTRAINT IF EXISTS redeem_codes_owner_player_id_fkey,
ADD CONSTRAINT redeem_codes_owner_player_id_fkey FOREIGN KEY (owner_player_id) REFERENCES public.profiles(id) ON DELETE SET NULL;
ALTER TABLE public.redeem_codes DROP CONSTRAINT IF EXISTS redeem_codes_created_by_player_fkey,
ADD CONSTRAINT redeem_codes_created_by_player_fkey FOREIGN KEY (created_by_player) REFERENCES public.profiles(id) ON DELETE SET NULL;
-- The redeem_creator check (a creator must be set) would now fail the very
-- delete above for a code a player created. Drop it; a code left with no
-- creator at all is revoked by this trigger instead.
CREATE OR REPLACE FUNCTION public.redeem_codes_orphan_guard() RETURNS trigger
LANGUAGE plpgsql SET search_path = public, pg_temp AS $$
BEGIN
IF NEW.created_by_player IS NULL AND NEW.created_by_admin IS NULL THEN
NEW.revoked_at := coalesce(NEW.revoked_at, now());
NEW.revoke_reason := coalesce(NEW.revoke_reason, 'creator account deleted');
END IF;
RETURN NEW;
END $$;
REVOKE EXECUTE ON FUNCTION public.redeem_codes_orphan_guard() FROM PUBLIC, anon, authenticated;
ALTER TABLE public.redeem_codes DROP CONSTRAINT IF EXISTS redeem_creator;
DROP TRIGGER IF EXISTS redeem_codes_orphan_guard ON public.redeem_codes;
CREATE TRIGGER redeem_codes_orphan_guard BEFORE INSERT OR UPDATE OF created_by_player, created_by_admin ON public.redeem_codes
FOR EACH ROW EXECUTE FUNCTION public.redeem_codes_orphan_guard();
-- 2. Group codes
CREATE UNIQUE INDEX IF NOT EXISTS groups_invite_code_canonical_uq
ON public.groups (public.redeem_canonical(invite_code)) WHERE invite_code IS NOT NULL;
ALTER TABLE public.groups ALTER COLUMN invite_code
SET DEFAULT translate(upper(substr(md5(random()::text || clock_timestamp()::text), 1, 6)), 'B', '7');
-- 3. Tournament codes register through the real rules
CREATE OR REPLACE FUNCTION public._redeem_tournament(p_player uuid, p_tid uuid, p_waive boolean)
RETURNS jsonb
LANGUAGE plpgsql
SET search_path TO 'public', 'pg_temp'
AS $function$
declare
v_t el3ab_tournaments%rowtype;
v_rid uuid;
v_rst text;
v_found boolean;
v_n int;
r jsonb;
base jsonb := jsonb_build_object('type', 'tournament_register', 'tournament_id', p_tid,
'fee_waived', coalesce(p_waive, false), 'waitlisted', false);
begin
if not coalesce(p_waive, false) then
r := tournament_register(p_tid, p_player);
return base || jsonb_build_object(
'ok', coalesce((r->>'ok')::boolean, false),
'applied', coalesce((r->>'ok')::boolean, false) and not coalesce((r->>'already')::boolean, false),
'already', coalesce((r->>'already')::boolean, false),
'registration_id', r->'registration_id',
'fee_charged', coalesce(r->'fee_charged', '0'::jsonb),
'reason', case r->>'code' when 'not_found' then 'tournament_not_found'
when 'full' then 'tournament_full'
else r->>'code' end);
end if;
-- Fee waived (pre-paid lists): the same gates as tournament_register, no charge.
select * into v_t from el3ab_tournaments where id = p_tid for update;
if not found or v_t.status = 'draft' then
return base || jsonb_build_object('ok', false, 'applied', false, 'reason', 'tournament_not_found');
end if;
if v_t.status <> 'registration'
or (v_t.registration_opens_at is not null and v_t.registration_opens_at > now())
or (v_t.registration_closes_at is not null and v_t.registration_closes_at < now()) then
return base || jsonb_build_object('ok', false, 'applied', false, 'reason', 'registration_closed');
end if;
select id, status into v_rid, v_rst from tournament_registrations
where tournament_id = p_tid and player_id = p_player for update;
v_found := found;
if v_found and v_rst = 'registered' then
return base || jsonb_build_object('ok', true, 'applied', false, 'already', true, 'registration_id', v_rid);
end if;
select count(*) into v_n from tournament_registrations where tournament_id = p_tid and status = 'registered';
if coalesce(v_t.max_players, 0) > 0 and v_n >= v_t.max_players then
return base || jsonb_build_object('ok', false, 'applied', false, 'reason', 'tournament_full');
end if;
if v_found then
update tournament_registrations set status = 'registered', withdrawn_at = null where id = v_rid;
else
insert into tournament_registrations (tournament_id, player_id, status, entry_fee_paid_coins)
values (p_tid, p_player, 'registered', 0) returning id into v_rid;
end if;
return base || jsonb_build_object('ok', true, 'applied', true, 'registration_id', v_rid,
'revived', v_found, 'player_count', v_n + 1);
end $function$;
REVOKE EXECUTE ON FUNCTION public._redeem_tournament(uuid, uuid, boolean) FROM PUBLIC, anon, authenticated;
GRANT EXECUTE ON FUNCTION public._redeem_tournament(uuid, uuid, boolean) TO service_role;
-- 4. Data
UPDATE public.game_plugins SET sort_order = CASE lower(name)
WHEN 'chess' THEN 1 WHEN 'ludo' THEN 2 WHEN 'dominoes' THEN 3 WHEN 'backgammon' THEN 4 ELSE sort_order END
WHERE lower(name) IN ('chess', 'ludo', 'dominoes', 'backgammon');
UPDATE public.ad_campaigns SET status = 'paused' WHERE name = 'Test Chess Ad' AND status = 'active';
COMMIT;
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment