Skip to content

Instantly share code, notes, and snippets.

@schinckel
Created February 22, 2015 00:24
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save schinckel/24b66c5a088c7776ca9f to your computer and use it in GitHub Desktop.
Save schinckel/24b66c5a088c7776ca9f to your computer and use it in GitHub Desktop.
Ten pin bowling score calculation
-- Game 1
INSERT INTO bowling.frame VALUES
(1, 1, 1, 7, 2, NULL),
(1, 1, 2, 3, 7, NULL),
(1, 1, 3, 6, 4, NULL),
(1, 1, 4, 10, NULL, NULL),
(1, 1, 5, 10, NULL, NULL),
(1, 1, 6, 10, NULL, NULL),
(1, 1, 7, 9, 1, NULL),
(1, 1, 8, 10, NULL, NULL),
(1, 1, 9, 8, 1, NULL),
(1, 1, 10, 6, 3, NULL);
-- Game 2
INSERT INTO bowling.frame VALUES
(2, 1, 1, 10, NULL, NULL),
(2, 1, 2, 3, 7, NULL),
(2, 1, 3, 10, NULL, NULL),
(2, 1, 4, 6, 4, NULL),
(2, 1, 5, 10, NULL, NULL),
(2, 1, 6, 9, 1, NULL),
(2, 1, 7, 10, NULL, NULL),
(2, 1, 8, 8, 2, NULL),
(2, 1, 9, 10, NULL, NULL),
(2, 1, 10, 7, 3, 10);
-- Game 3
INSERT INTO bowling.frame VALUES
(3, 1, 1, 10, NULL, NULL),
(3, 1, 2, 10, NULL, NULL),
(3, 1, 3, 10, NULL, NULL),
(3, 1, 4, 10, NULL, NULL),
(3, 1, 5, 10, NULL, NULL),
(3, 1, 6, 10, NULL, NULL),
(3, 1, 7, 10, NULL, NULL),
(3, 1, 8, 10, NULL, NULL),
(3, 1, 9, 10, NULL, NULL),
(3, 1, 10, 10, 10, 10);
CREATE VIEW pretty_print AS (
WITH symbols AS (
SELECT
game_id, player_id, frame,
CASE WHEN ball1 = 10 THEN 'X' ELSE ball1::text END as ball1,
CASE WHEN ball2 IS NULL THEN ' '
WHEN ball1 + ball2 = 10 THEN '/'
WHEN ball1 = 10 AND ball2 = 10 THEN 'X'
ELSE ball2::text
END as ball2,
CASE WHEN ball3 IS NULL THEN ' '
WHEN ball3 = 10 THEN 'X'
WHEN ball3 + ball2 = 10 THEN '/'
ELSE ball3::text
END as ball3,
lpad(total::text, 5, ' ') as total
FROM
frame_score
ORDER BY game_id, player_id, frame
), grouped_data AS (
SELECT
game_id,
player_id,
array_agg(ball1) ball1,
array_agg(ball2) ball2,
array_agg(ball3) ball3,
array_agg(total) total
FROM
symbols
GROUP BY
game_id, player_id
)
SELECT
game_id,
player_id,
ball1[1] || ' | ' || ball2[1] || ' ' || chr(10) || total[1] AS "1",
ball1[2] || ' | ' || ball2[2] || ' ' || chr(10) || total[2] AS "2",
ball1[3] || ' | ' || ball2[3] || ' ' || chr(10) || total[3] AS "3",
ball1[4] || ' | ' || ball2[4] || ' ' || chr(10) || total[4] AS "4",
ball1[5] || ' | ' || ball2[5] || ' ' || chr(10) || total[5] AS "5",
ball1[6] || ' | ' || ball2[6] || ' ' || chr(10) || total[6] AS "6",
ball1[7] || ' | ' || ball2[7] || ' ' || chr(10) || total[7] AS "7",
ball1[8] || ' | ' || ball2[8] || ' ' || chr(10) || total[8] AS "8",
ball1[9] || ' | ' || ball2[9] || ' ' || chr(10) || total[9] AS "9",
ball1[10] || ' | ' || ball2[10] || ' | ' || ball3[10] || ' ' || chr(10) || lpad(total[10], 9, ' ') AS "10"
FROM grouped_data
);
DROP SCHEMA bowling CASCADE;
BEGIN;
CREATE SCHEMA bowling;
SET search_path TO bowling;
CREATE DOMAIN bowling.frame_number AS integer
CHECK ('[1,10]'::int4range @> VALUE)
NOT NULL;
CREATE DOMAIN bowling.ball AS integer
CHECK ('[0,10]'::int4range @> VALUE);
CREATE TABLE bowling.frame
(
game_id INTEGER NOT NULL,
player_id INTEGER NOT NULL,
frame bowling.frame_number NOT NULL,
ball1 bowling.ball NOT NULL,
ball2 bowling.ball NULL,
ball3 bowling.ball NULL,
PRIMARY KEY (game_id, player_id, frame)
);
ALTER TABLE bowling.frame
ADD CONSTRAINT max_spare_unless_frame_10_strike CHECK
(
ball1 + ball2 <= 10 OR (frame = 10 AND ball1 = 10)
);
ALTER TABLE bowling.frame
ADD CONSTRAINT ball_2_never_bowled_after_strike CHECK
(
ball2 IS NULL OR ball1 < 10 OR frame = 10
);
ALTER TABLE bowling.frame
ADD CONSTRAINT ball_3_only_in_frame_10 CHECK
(
ball3 IS NULL OR frame = 10
);
ALTER TABLE bowling.frame
ADD CONSTRAINT ball3_only_if_eligible CHECK
(
ball3 IS NULL OR (ball2 IS NOT NULL AND ball1 + ball2 >= 10)
);
ALTER TABLE bowling.frame
ADD CONSTRAINT ball3_max_spare_or_strike CHECK
(
ball2 + ball3 <= 10
OR
ball1 + ball2 = 20
OR
ball1 + ball2 = 10
);
CREATE OR REPLACE VIEW bowling.frame_score AS (
WITH pin_counts AS (
SELECT
game_id,
player_id,
frame,
ball1, ball2, ball3,
LEAD(ball1, 1) OVER (
PARTITION BY game_id, player_id
ORDER BY frame
) AS next_ball_1,
LEAD(ball2, 1) OVER (
PARTITION BY game_id, player_id
ORDER BY frame
) AS next_ball_2,
LEAD(ball1, 2) OVER (
PARTITION BY game_id, player_id
ORDER BY frame
) AS next_next_ball_1
FROM bowling.frame
),
frame_counts AS (
SELECT
game_id,
player_id,
frame,
ball1, ball2, ball3,
CASE WHEN frame = 10 AND ball1 + ball2 >= 10 THEN
ball1 + ball2 + ball3
WHEN ball1 = 10 THEN
ball1 + next_ball_1 + (
CASE WHEN next_ball_1 = 10 AND frame < 9 THEN
next_next_ball_1
ELSE
next_ball_2
END
)
WHEN ball1 + ball2 = 10 THEN
ball1 + ball2 + next_ball_1
ELSE
ball1 + ball2
END AS score
FROM pin_counts
)
SELECT *,
CASE WHEN score IS NOT NULL THEN
SUM(score) OVER (
PARTITION BY game_id, player_id
ORDER BY frame
ROWS UNBOUNDED PRECEDING
)
ELSE NULL END AS total
FROM frame_counts
);
COMMIT;
INSERT INTO bowling.frame VALUES(1, 2, 0, 9, NULL, NULL);
INSERT INTO bowling.frame VALUES(1, 2, 1, 11, NULL, NULL);
INSERT INTO bowling.frame VALUES(1, 2, 1, -1, NULL, NULL);
INSERT INTO bowling.frame VALUES(1, 2, 1, 8, 3, NULL);
INSERT INTO bowling.frame VALUES(1, 2, 1, 8, 1, 1);
INSERT INTO bowling.frame VALUES(1, 2, 1, 8, 2, 1);
INSERT INTO bowling.frame VALUES(1, 2, 10, 8, 3, 1);
INSERT INTO bowling.frame VALUES(1, 2, 10, 8, 2, 11);
INSERT INTO bowling.frame VALUES(1, 2, 10, 10, NULL, 10);
INSERT INTO bowling.frame VALUES(1, 2, 5, 10, 0, NULL);
INSERT INTO bowling.frame VALUES(1, 2, 10, 10, 2, 9);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment