The search for simpler code while having fun.

I denna tredje del implementerar vi en algoritm som beräknar alla giltiga flytt för en piece (Tetromino) i sin startposition. Vi passar även på att förbättra läsbarheten för delar av koden som även fortsättningsvis skrivs i Clojure och Python tillsammans med den komponentbaserade arkitekturen Polylith.
Tidigare delar:
piece och board.Här hittar du all kod för denna post:
Tetris har släppt i en rad olika varianter, så som den handhållna Game Boy, konsollen Nintento NES, och detta arkadspel från Atari, som jag i mina unga dagar spelade ohälsosamt mycket på en numera nedlagd biljardhall!
Alla varianter uppför sig lite olika vad gäller vilka färger bitarna har, var de startar och hur de roterar, med mera.
I de flesta varianter av Tetris startar bitarna i följande rotationslägen (liggande) innan de börjar falla:

Var på brädet bitarna startar varierar också mellan de olika varianterna av Tetris. Till exempel så startar bitarna i femte x-position i Nintendo NES och Atari Arcade, medan de startar i fjärde x-position i Game Boy:

I dessa äldre varianter av Tetris roterar bitarna endast moturs, till skillnad mot vissa nyare spel där man kan rotera både med- och moturs.
Här jämför vi hur bitarna roterar för de tre nämnda varianterna av Tetris:

För Atari är bitarna orienterade mot övre vänstra hörnet (undantaget stående I), medan de mestadels roterar runt sitt centrum i de andra två varianterna.
I vår kod reprecenterar vi en piece genom att ange fyra [x y] celler:
[[0 1] [1 1] [2 1] [1 2]]
Denna representation är enkel för koden att hantera, men kommunicerar dåligt för en människa vilken form en piece har.
Grundregeln är att kod ska skrivas så att den blir enkel att förstå för de som läser och ändrar den (människor och på senare tid även AI-Agenter).
Låt oss därför definiera en piece så här istället:
(def T0 ['---
'xxx
'-x-])
Python:
T0 = [
"---",
"xxx",
"-x-",
]
Nu kan vi definiera alla sju pieces och deras rotationslägen för Game Boy (Python-koden är nästan identisk):
(ns tetrisanalyzer.piece.settings.game-boy
(:require [tetrisanalyzer.piece.shape :as shape]))
(def O0 ['----
'-xx-
'-xx-
'----])
(def I0 ['----
'----
'xxxx
'----])
(def I1 ['-x--
'-x--
'-x--
'-x--])
(def Z0 ['---
'xx-
'-xx])
(def Z1 ['-x-
'xx-
'x--])
(def S0 ['---
'-xx
'xx-])
(def S1 ['x--
'xx-
'-x-])
(def J0 ['---
'xxx
'--x])
(def J1 ['-xx
'-x-
'-x-])
(def J2 ['x--
'xxx
'---])
(def J3 ['-x-
'-x-
'xx-])
(def L0 ['---
'xxx
'x--])
(def L1 ['-x-
'-x-
'-xx])
(def L2 ['--x
'xxx
'---])
(def L3 ['xx-
'-x-
'-x-])
(def T0 ['---
'xxx
'-x-])
(def T1 ['-x-
'-xx
'-x-])
(def T2 ['-x-
'xxx
'---])
(def T3 ['-x-
'xx-
'-x-])
(def pieces [[O0]
[I0 I1]
[Z0 Z1]
[S0 S1]
[J0 J1 J2 J3]
[L0 L1 L2 L3]
[T0 T1 T2 T3]])
(def shapes (shape/shapes pieces))
Den avslutande shapes funktionen omvandlar bitarna till det format koden använder:
[;; O
[[[1 1] [2 1] [1 2] [2 2]]]
;; I
[[[0 2] [1 2] [2 2] [3 2]]
[[1 0] [1 1] [1 2] [1 3]]]
;; Z
[[[0 1] [1 1] [1 2] [2 2]]
[[1 0] [0 1] [1 1] [0 2]]]
;; S
[[[1 1] [2 1] [0 2] [1 2]]
[[0 0] [0 1] [1 1] [1 2]]]
;; J
[[[0 1] [1 1] [2 1] [2 2]]
[[1 0] [2 0] [1 1] [1 2]]
[[0 0] [0 1] [1 1] [2 1]]
[[1 0] [1 1] [0 2] [1 2]]]
;; L
[[[0 1] [1 1] [2 1] [0 2]]
[[1 0] [1 1] [1 2] [2 2]]
[[2 0] [0 1] [1 1] [2 1]]
[[0 0] [1 0] [1 1] [1 2]]]
;; T
[[[0 1] [1 1] [2 1] [1 2]]
[[1 0] [1 1] [2 1] [1 2]]
[[1 0] [0 1] [1 1] [2 1]]
[[1 0] [0 1] [1 1] [1 2]]]]
Så här ser testet för shape-funktionen ut:
(ns tetrisanalyzer.piece.shape-test
(:require [clojure.test :refer :all]
[tetrisanalyzer.piece.shape :as shape]))
(deftest converts-a-piece-shape-grid-to-a-vector-of-xy-cells
(is (= [[2 0]
[1 1]
[2 1]
[1 2]]
(shape/shape ['--x-
'-xx-
'-x--
'----]))))
Python:
from tetrisanalyzer.piece import shape
def test_converts_a_piece_shape_grid_to_a_list_of_xy_cells():
assert [
[2, 0],
[1, 1],
[2, 1],
[1, 2],
] == shape.shape(
[
"--x-",
"-xx-",
"-x--",
"----",
]
)
Implementation i Clojure:
(ns tetrisanalyzer.piece.shape)
(defn cell [x character y]
(when (= \x character)
[x y]))
(defn row-cells [y row]
(keep-indexed #(cell %1 %2 y)
(str row)))
(defn shape [piece-grid]
(vec (mapcat identity
(map-indexed row-cells piece-grid))))
(defn shapes [piece-grids]
(mapv #(mapv shape %)
piece-grids))
Om du är ovan med Clojure, kommer här förklarande exempel till ett par av funktionerna:
(map-indexed vector ["I" "love" "Tetris"])
;; ([0 "I"] [1 "love"] [2 "Tetris"])
Funktionen map-indexed itererar över elementen "I", "love", och "Tetris", och bygger upp en ny lista där varje elment skapas genom anrop till funktionen vector med indexet som räknas upp, vilket motsvarar:
(list (vector 0 "I")
(vector 1 "love")
(vector 2 "tetris"))
;; ([0 "I"] [1 "love"] [2 "Tetris"])
Funktionen keep-indexed fungerar på samma sätt, men tar bara med värden som ej är nil, därav användningen av when:
;; %1 = first argument (index)
;; %2 = second argument (value)
(keep-indexed #(when %2 [%1 %2])
["I" nil "Tetris"])
;; ([0 "I"] [2 "Tetris"])
Implementation i Python:
def shape(piece_grid):
cells = []
for y, row in enumerate(piece_grid):
for x, ch in enumerate(row):
if ch == "x":
cells.append([x, y])
return cells
def shapes(pieces_grids):
return [
[shape(piece-grid) for piece_grid in piece_grids]
for piece_grids in pieces_grids
]
Här använder vi list comprehension för att konvertera till [x, y] celler. Funktionen enumerate har samma funktion som map-indexed i Clojure, att lägga till ett index (0, 1, 2, ...) för varje element.
Den nya koden som beräknar giltiga flytt av en piece i sin startposition, behöver ligga någonstans. Vi behöver kunna flytta och rotera en piece, och testa om platsen är ledig på en board.
Efter lite experimenterande, landade jag i att lägga denna kod under ett eget placement-paket i piece-komponenten och exponera funktionen placements i dess interface. Dessutom fick set-piece bo i piece istället för board:

Inom varje komponent listar vi vad som ingår i dess interface (vad som är publikt) medan pilen visar att piece anropar funktioner i board.
Implementationen delar vi upp i namespacen move, placement, och visit:
▾ tetris-polylith
▸ bases
▾ components
▸ board
▾ piece
▾ src
▾ placement
move.clj
placement.clj
visit.clj
▸ settings
bitmask
interface.clj
piece
shape
▾ test
▾ placement
move_test.clj
placement_test.clj
visit_test.clj
piece_test.clj
shape_test.clj
▸ development
▸ projects
Så här ser move-test ut:
(ns tetrisanalyzer.piece.placement.move-test
(:require [clojure.test :refer :all]
[tetrisanalyzer.piece.piece :as piece]
[tetrisanalyzer.piece.placement.move :as move]
[tetrisanalyzer.piece.bitmask :as bitmask]
[tetrisanalyzer.board.interface :as board]
[tetrisanalyzer.piece.settings.atari-arcade :as atari-arcade]))
(def x 2)
(def y 1)
(def rotation 0)
(def S piece/S)
(def shapes atari-arcade/shapes)
(def bitmask (bitmask/rotation-bitmask shapes S))
(def piece (piece/piece S rotation shapes))
(def board (board/board ['xxxxxxxx
'xxx--xxx
'xx--xxxx
'xxxxxxxx]))
(deftest valid-move
(is (= true
(move/valid-move? board x y S rotation shapes))))
(deftest valid-left-move
(is (= [2 1 0]
(move/left board (inc x) y S rotation nil shapes))))
(deftest invalid-left-move
(is (= nil
(move/left board x y S rotation nil shapes))))
(deftest valid-right-move
(is (= [2 1 0]
(move/right board (dec x) y S rotation nil shapes))))
(deftest invalid-right-move
(is (= nil
(move/right board x (dec y) S rotation nil shapes))))
(deftest unoccupied-down-move
(is (= [[2 1 0] nil]
(move/down board x (dec y) S rotation nil shapes))))
(deftest down-move-hits-ground
(is (= [nil [[2 1 0]]]
(move/down board x y S rotation nil shapes))))
(deftest valid-rotation
(is (= [2 1 0]
(move/rotate board x y S (dec rotation) bitmask shapes))))
(deftest invalid-rotation-without-kick
(is (= nil
(move/rotate board (inc x) y S (inc rotation) bitmask shapes))))
(deftest valid-rotation-with-kick
(is (= [2 1 0]
(move/rotate-with-kick board (inc x) y S (inc rotation) bitmask shapes))))
(deftest invalid-move-outside-board
(is (= false
(move/valid-move? board 10 -10 S rotation shapes))))
Det första testet valid-move validerar att S-biten:
['-xx
'xx-]
Kan placers på position x=2, y=1:
['xxxxxxxx
'xxx--xxx
'xx--xxxx
'xxxxxxxx]
Utöver det, testas olika varianter av giltiga flytt och rotation in till det tomma området, samt ogiltiga flytt utanför området.
I Tetris finns det något som kallas kick, eller wall kick. När man roterar en bit och positionen är upptagen på brädet, kommer även x-1 att testas (ett steg till vänster). På Nintendo NES är detta avslaget, medan den är aktiverad på de övriga två varianterna vi stödjer här. På nyare Tetris testas ibland även andra placeringar än x-1.
Implementationen ser ut så här:
(ns tetrisanalyzer.piece.placement.move
(:require [tetrisanalyzer.piece.piece :as piece]))
(defn cell [board x y [cx cy]]
(or (get-in board [(+ y cy) (+ x cx)])
piece/X))
(defn valid-move? [board x y p rotation shapes]
(every? zero?
(map #(cell board x y %)
(piece/piece p rotation shapes))))
(defn left [board x y p rotation _ shapes]
(when (valid-move? board (dec x) y p rotation shapes)
[(dec x) y rotation]))
(defn right [board x y p rotation _ shapes]
(when (valid-move? board (inc x) y p rotation shapes)
[(inc x) y rotation]))
(defn down
"Returns [down-move placement] where:
- down-move: next move when moving down or nil if blocked
- placement: final placement if blocked, or nil if can move down"
[board x y p rotation _ shapes]
(if (valid-move? board x (inc y) p rotation shapes)
[[x (inc y) rotation] nil]
[nil [[x y rotation]]]))
(defn rotate [board x y p rotation bitmask shapes]
(let [new-rotation (bit-and (inc rotation) bitmask)]
(when (valid-move? board x y p new-rotation shapes)
[x y new-rotation])))
(defn rotate-with-kick [board x y p rotation bitmask shapes]
(or (rotate board x y p rotation bitmask shapes)
(rotate board (dec x) y p rotation bitmask shapes)))
(defn rotation-fn [rotation-kick?]
(if rotation-kick?
rotate-with-kick
rotate))
Funktionerna är ganska enkla som synes, så låt oss istället titta på koden som hjälper oss att hålla reda på vilka flytt som redan besökts:
(ns tetrisanalyzer.piece.placement.visit)
(defn visited? [visited-moves x y rotation]
(if-let [visited-rotations (get-in visited-moves [y x])]
(not (zero? (bit-and visited-rotations
(bit-shift-left 1 rotation))))
true)) ;; Cells outside the board are treated as visited
(defn visit [visited-moves x y rotation]
(assoc-in visited-moves [y x] (bit-or (get-in visited-moves [y x])
(bit-shift-left 1 rotation))))
Anropet till standardfunktionen bit-shift-left returnerar en satt bit i någon av de fyra lägsta bitarna:
| rotation | bit |
|---|---|
| 0 | 0001 |
| 1 | 0010 |
| 2 | 0100 |
| 3 | 1000 |
Dessa "flaggor" används sedan för att markera att vi besökt ett visst [x y rotation] flytt på ett bräde. Notera att vi skickar in ett "besöks-bräde" (visited-moves) till visited och får tillbaka en kopia där [x y] cellen har en bit satt för angiven rotation. Denna "kopiering" är dock väldigt snabb och minneseffektiv, se "structural sharing" under Data Structures.
Testerna ser ut så här:
(ns tetrisanalyzer.piece.placement.visit-test
(:require [clojure.test :refer :all]
[tetrisanalyzer.piece.placement.visit :as visit]))
(def x 2)
(def y 1)
(def rotation 3)
(def unvisited [[0 0 0 0]
[0 0 0 0]])
(deftest move-is-not-visited
(is (= false
(visit/visited? unvisited x y rotation))))
(deftest move-is-visited
(let [visited (visit/visit unvisited x y rotation)]
(is (= true
(visit/visited? visited x y rotation)))))
Python:
from tetrisanalyzer.piece import placement
X = 2
Y = 1
ROTATION = 3
UNVISITED = [
[0, 0, 0, 0],
[0, 0, 0, 0],
]
def test_move_is_not_visited():
assert placement.visit.is_visited(UNVISITED, X, Y, ROTATION) is False
def test_move_is_visited():
visited = [row[:] for row in UNVISITED]
placement.visit.visit(visited, X, Y, ROTATION)
assert placement.visit.is_visited(visited, X, Y, ROTATION) is True
Nu har vi bäddat för att kunna implementera funktionen placements som räknar ut alla giltiga flytt för en piece i sin startposition.
Vi börjar med testet:
(ns tetrisanalyzer.piece.placement.placement-test
(:require [clojure.test :refer :all]
[tetrisanalyzer.piece.piece :as piece]
[tetrisanalyzer.piece.placement.placement :as placement]
[tetrisanalyzer.piece.settings.atari-arcade :as atari-arcade]))
(def start-x 2)
(def sorter (juxt second first last))
(def board [[0 0 0 0 0 0]
[0 0 1 1 0 0]
[0 0 1 0 0 1]
[0 0 1 1 1 1]])
(def shapes atari-arcade/shapes)
;; Start position of the J piece:
;; --JJJ-
;; --xxJ-
;; --x--x
;; --xxxx
(deftest placements--without-rotation-kick
(is (= [[2 0 0]
[3 0 0]]
(sort-by sorter (placement/placements board piece/J start-x false shapes)))))
;; With rotation kick, checking if x-1 fits:
;; -JJ---
;; -Jxx--
;; -Jx--x
;; --xxxx
(deftest placements--with-rotation-kick
(is (= [[1 0 1]
[2 0 0]
[3 0 0]
[0 1 1]]
(sort-by sorter (placement/placements board piece/J start-x true shapes)))))
Detta testar att vi får tillbaka de giltiga [x y rotation]-positioner som en piece kan placeras på ett bräde utifrån sin startposition.
Implementationen:
(ns tetrisanalyzer.piece.placement.placement
(:require [tetrisanalyzer.piece.placement.move :as move]
[tetrisanalyzer.piece.placement.visit :as visit]
[tetrisanalyzer.board.interface :as board]
[tetrisanalyzer.piece.bitmask :as bitmask]))
(defn ->placements [board x y p rotation bitmask valid-moves visited-moves rotation-fn shapes]
(loop [next-moves (list [x y rotation])
placements []
valid-moves valid-moves
visited-moves visited-moves]
(if-let [[x y rotation] (first next-moves)]
(let [next-moves (rest next-moves)]
(if (visit/visited? visited-moves x y rotation)
(recur next-moves placements valid-moves visited-moves)
(let [[down placement] (move/down board x y p rotation bitmask shapes)
moves (keep #(% board x y p rotation bitmask shapes)
[move/left
move/right
rotation-fn
(constantly down)])]
(recur (into next-moves moves)
(concat placements placement)
(conj valid-moves [x y rotation])
(visit/visit visited-moves x y rotation)))))
placements)))
(defn placements [board p x kick? shapes]
(let [y 0
rotation 0
bitmask (bitmask/rotation-bitmask shapes p)
visited-moves (board/empty-board board)
rotation-fn (move/rotation-fn kick?)]
(if (move/valid-move? board x y p rotation shapes)
(->placements board x y p rotation bitmask [] visited-moves rotation-fn shapes)
[])))
Vi börjar med att förklara följande sektion i ->placements:
(loop [next-moves (list [x y rotation])
placements []
valid-moves valid-moves
visited-moves visited-moves]
Dessa fyra rader initierar det data vi vill loopa på, där next-moves är den lista med moves vi behöver gå igenom och som växer och betas av löpande, och där placements ackumulerar giltiga flytt.
Då Clojure ej stödjer tail-recursion, finns istället loop att tillgå som alternativ till en rekursiv lösning, vilket vi vill undvika för att inte få problem med stack overflow om vi skulle använda bräden större än 10x20.
(if-let [[x y rotation] (first next-moves)]
Hämtar nästa move från next-moves och fortsätter med koden direkt under, eller returnerar placements (sista raden i funktionen) om next-moves är tom, vilket är alla giltiga flytt.
(let [next-moves (rest next-moves)]
Drop:ar första elementet i next-moves, som vi precis har plockat ut.
(if (visit/visited? visited-moves x y rotation)
Om flyttet redan besökts, fortsätt med:
(recur next-moves placements valid-moves visited-moves)
Som fortsätter vår sökning efter giltiga flytt (raden efter (loop [...]), genom att gå till nästa flytt att utvärdera.
Annars, om flyttet ej har besökts, fortsätt med:
(let [[down placement] (move/down board x y p rotation bitmask shapes)
...]
Sätter down till nästa nedåtflytt (om ledigt) eller placement om det ej gick att flytta nedåt, vilket händer när vi nått botten eller om någon del av "bygget" är i vägen.
För dessa rader:
(keep #(% board x y p rotation bitmask shapes)
[move/left
move/right
rotation-fn
(constantly down)])
Kommer %-tecknet ersättas med funktionerna i vektorn, vilket motsvarar:
[(move/left board x y p rotation bitmask shapes)
(move/right board x y p rotation bitmask shapes)
(rotation-fn board x y p rotation bitmask shapes)
(down board x y p rotation bitmask shapes)]
Dessa funktionsanrop kommer att utföra alla möjliga flytt, inklusive en rotation, som alla returnerar [x y rotation] om positionen var ledig på brädet, eller nil om upptagen. Funktionen keep filtrerar bort nil-värden, vilket är alla giltiga flytt som sedan tilldelas moves.
Till slut körs:
(recur (into next-moves moves)
(concat placements placement)
(conj valid-moves [x y rotation])
(visit/visit visited-moves x y rotation))
Vilket anropar loop igen med:
next-moves med de eventuellt nya moves tillagdaplacements med det eventuellt giltiga flyttet tillagtvalid-moves med aktuellt move tillagtvisited-moves där aktuellt move är markerat som besöktDetta snurrar på, tills next-moves är tom, och då returneras placements.
Funktionen som kickar igång allt, och returnerar giltiga flytt för en piece i sin startposition:
(defn placements [board p x kick? shapes]
(let [y 0
rotation 0
bitmask (bitmask/rotation-bitmask shapes p)
visited-moves (board/empty-board board)
rotation-fn (move/rotation-fn kick?)]
(if (move/valid-move? board x y p rotation shapes)
(->placements board x y p rotation bitmask [] visited-moves rotation-fn shapes)
[])))
board: en tvådimensionell vektor som representerar en board, oftast 10x20.p: piece index (0, 1, 2, 3, 4, 5, eller 6).x: vilken kolumn 4x4-griden startar (där biten finns). Första kolumnen är 0.y: satt till 0 (översta raden) och anger startrad för 4x4-griden.rotation satt till 0 (start-rotation).bitmask: används vid uppräkning av rotation så att den börjar om på 0 när nått slutet på det antal rotationer den kan göra.visited-moves: har samma struktur som en board, d.v.s en tvådimensionell array av vektorer, oftast 10x20.rotation-fn: returnerar rätt rotationsfunktion beroende på om kick är aktivt eller ej. Testar även position x-1 om kick? är true.shapes: formerna för alla pieces och dess rotationslägen, lagrade som [x y] celler.(if (move/valid-move? board x y p rotation shapes): vi måste testa ifall den initiala positionen är ledig eller ej, och returnera en tom lista (vektor) om ej ledig.(->placements board x y p rotation bitmask [] visited-moves rotation-fn shapes) beräknar de giltiga flytten.Implementation i Python:
from collections import deque
from tetrisanalyzer import board as board_ifc, piece
from . import move, visit
def _placements(board, x, y, p, rotation, bitmask, valid_moves, visited_moves, rotation_move_fn, shapes):
next_moves = deque([[x, y, rotation]])
placements = []
while next_moves:
x, y, rotation = next_moves.popleft()
if visit.is_visited(visited_moves, x, y, rotation):
continue
down_move, placement = move.down(board, x, y, p, rotation, bitmask, shapes)
moves = [
move.left(board, x, y, p, rotation, bitmask, shapes),
move.right(board, x, y, p, rotation, bitmask, shapes),
rotation_move_fn(board, x, y, p, rotation, bitmask, shapes),
down_move,
]
moves = [move for move in moves if move is not None]
next_moves.extend(moves)
if placement is not None:
placements.extend(placement)
valid_moves.append([x, y, rotation])
visit.visit(visited_moves, x, y, rotation)
return placements
def placements(board, p, start_x, kick, shapes):
bitmask = piece.bitmask.rotation_bitmask(shapes, p)
visited_moves = board_ifc.empty_board_as(board)
rotation_move_fn = move.rotation_fn(kick)
if not move.is_valid_move(board, start_x, 0, p, 0, shapes):
return []
return _placements(board, start_x, 0, p, 0, bitmask, [], visited_moves, rotation_move_fn, shapes)
Koden följer samma algoritm som i Clojure. Funktionen deque använder vi då den är lite snabbare än en lista, då vi både gör popleft och extend.
Avslutningsvis kör vi våra tester:
$> cd ~/source/tetrisanalyzer/langs/clojure/tetris-polylith
$> poly test :dev
Projects to run tests from: development
Running tests for the development project using test runner: Polylith built-in clojure.test runner...
Running tests from the development project, including 2 bricks: board, piece
Testing tetrisanalyzer.board.core-test
Ran 1 tests containing 1 assertions.
0 failures, 0 errors.
Test results: 1 passes, 0 failures, 0 errors.
Testing tetrisanalyzer.board.clear-rows-test
Ran 1 tests containing 1 assertions.
0 failures, 0 errors.
Test results: 1 passes, 0 failures, 0 errors.
Testing tetrisanalyzer.board.grid-test
Ran 2 tests containing 2 assertions.
0 failures, 0 errors.
Test results: 2 passes, 0 failures, 0 errors.
Testing tetrisanalyzer.piece.placement.placement-test
Ran 2 tests containing 2 assertions.
0 failures, 0 errors.
Test results: 2 passes, 0 failures, 0 errors.
Testing tetrisanalyzer.piece.placement.move-test
Ran 11 tests containing 11 assertions.
0 failures, 0 errors.
Test results: 11 passes, 0 failures, 0 errors.
Testing tetrisanalyzer.piece.placement.visit-test
Ran 2 tests containing 2 assertions.
0 failures, 0 errors.
Test results: 2 passes, 0 failures, 0 errors.
Testing tetrisanalyzer.piece.shape-test
Ran 1 tests containing 1 assertions.
0 failures, 0 errors.
Test results: 1 passes, 0 failures, 0 errors.
Testing tetrisanalyzer.piece.piece-test
Ran 1 tests containing 1 assertions.
0 failures, 0 errors.
Test results: 1 passes, 0 failures, 0 errors.
Execution time: 0 seconds
Python:
$> cd ~/source/tetrisanalyzer/langs/python/tetris-polylith-uv
$> uv run pytest
=================================================================================== test session starts ===================================================================================
platform darwin -- Python 3.13.11, pytest-9.0.2, pluggy-1.6.0
rootdir: /Users/tengstrand/source/tetrisanalyzer/langs/python/tetris-polylith-uv
configfile: pyproject.toml
collected 21 items
test/components/tetrisanalyzer/board/test_clear_rows.py . [ 4%]
test/components/tetrisanalyzer/board/test_core.py .. [ 14%]
test/components/tetrisanalyzer/board/test_grid.py .. [ 23%]
test/components/tetrisanalyzer/piece/placement/test_move.py ........... [ 76%]
test/components/tetrisanalyzer/piece/placement/test_placement.py .. [ 85%]
test/components/tetrisanalyzer/piece/placement/test_visit.py .. [ 95%]
test/components/tetrisanalyzer/piece/test_shape.py . [100%]
=================================================================================== 21 passed in 0.02s ====================================================================================
Alla tester gick igenom!
I denna tredje del tog jag mig an den inte helt lätta uppgiften att räkna ut alla giltiga flytt för en bit i sin startposition.
Jag undvek att implementera det som en rekursiv algoritm, då vi annars blir begränsade i hur stora bräden vi kan hantera.
Den nya koden fick bo i piece, efter en del experimenterande.
Vi passade även på att göra koden lättare att jobba med, genom att specificera bitarna på ett mer läsbart sätt, och med den ändringen kunde vi enkelt stödja tre olika varianter av Tetris.
Hoppas du hade lika kul som jag 😃
Happy coding!
Published: 2026-02-11