Puyo Puyo Tsu/Rotation, collision and push back
Rotation handling is performed on the frame the player's input was detected, and also performs two other operations:
- collision detection, the player's trying to rotate against a wall, or between two obstructing columns;
- push back, shifting the player's pair whenever possible if he tries to rotate against a blocking element.
This page fully details the rotation-related routines of the game.
Frame data for rotation animations are detailed on the dedicated page.
Overview
Here's an overview of the routine handling rotation, rotation_start():
Click on the image to see the fully commented assembly code. It's a pretty long routine that covers various steps:
- get gamepad settings and read the input;
- check the destination cell for the rotating puyo (target cell);
- check the opposite cell from the target cell, relative to the main puyo;
- handle double-rotation (double tapping the buttons);
- push the falling pair back if necessary;
- acknowledge the rotation and trigger the rotation animation.
Rotation basics
To better understand what the routine performs, here's a visual introduction on how the game depicts various elements of the falling pair:
(click on the image for a full size view)
The reference point is located at the top left corner of the player's board, starting at (0,0) all the way to (5,13). That makes 14 rows and 6 columns worth of space for your puyos. Row #y=0 and row #y=1 are the two ghost rows in which you can put puyos that don't count towards your chains, unless they fall into the visible board space below (rows #y=2 to 13).
A falling pair is handled by the game in two parts:
- the main puyo (yellow and highlighted puyo), center of rotation, is the one the player directly controls through the controller's D-pad. It gets is full coordinates stored in a dedicated memory structure;
- the "slave" puyo (red puyo), which has a mostly similar in-memory structure, linked to the main one, but it doesn't store the slave puyo's full coordinates.
The main puyo's structure holds a field describing a "rotation ID". This field holds a value between 0 and 3, each referring to a particular rotation. This is depicted in the annotated screenshot above, with P1 having a rotation ID of 0, while P2 has a rotation ID of 3. This ID rather serves as a geometric transformation "identifier". You can safely assume its values are mapped to the corresponding rotated coordinates of the slave puyo, but they have been carefully picked to allow for clever math afterwards (discussed below), when dealing with collision detection and pushing back the falling pair.
Refer to the memory structures page for a complete description of the structure's fields.
Red spots around P1's main puyo mark the diagonal cells, while blue spots below P2's pair mark the cells the game will check for collision when deciding if it should finally place the falling pair on the board (this check is done in another routine).
Routine steps
It is recommended that you keep the routine code overview (see above) open while reading this section, to get a grasp at the code flow at stake.
Gamepad settings and readout
Starting at 0x6296, the routine gets the current player ID to get this player's gamepad configuration (type), and map the rotation buttons accordingly. After mapping the button into d0 (counter-clockwise) and d1 (clockwise), the routine matches counter-clockwise buttons with new inputs on that frame.
This means the rotation events are only triggered if the player has just pushed the button; no input-repeat mechanism will occur. But this also mean one can't input two rotations on two consecutive frames, because of how the gamepad status variable at plyr_kstatus+1 (actually named plyr_newkeys) is populated (see Puyo Puyo Tsu/Gamepad Input).
From 0x62C2 onwards, the routine checks which button has been pressed, and discards the rotation event (beq locret_6338) if both buttons are pressed at the same time.
Each of the two branches correspond to a rotation direction, and initialize the d0 and d1 registers with appropriate values (negative or positive values). This is the first clever use of the transform IDs: rotating clockwise means incrementing the ID, while decrementing it will account for a counter-clockwise rotation.
Target cell check
At 0x62D6, this incrementation/decrementation is performed, with d0 now being the "target" rotation ID. Original (current) rotation ID is a byte read from the memory, at the address a0+0x2B ($2B(a0) in the disassembly).
The resulting value is masked so that only its lowest two bits are kept (remainder of a euclidian division by 4). This trick allows rotating clockwise from rotation ID #3 to go back to 0. Decrementing from 0 will result in a binary value consisting of only 1's, with the lowest two bits accounting for the decimal value "3".
The value is copied to d3. Both d0 and d3 now hold the target transformation ID. This value has not yet been committed to memory, thus has not been acknowledged yet. The game wants to check whether or not this is a valid move.
The first check occurs immediately afterward: the check_target_and_diag() routine, takes a desired transformation ID, and will check both the content of the desired cell, and the diagonal cell between the original (current) cell of the slave puyo and its intended destination (see the red spots on the earlier screenshot).
The check_target_and_diag() routine returns a result in the lowest two bits of d0:
- bit #0: if set to 1, the target cell is full;
- bit #1: if set to 1, the diagonal cell is full.
But for now, the game only cares about the status of the target cell for the desired rotation. If the cell is empty, the routine skips directly to the rotation acknowledgement and performs the action accordingly.
It gets more complicated if the target cell is full: we have a collision and the game will try to decide how to push back the player's pair, if possible.
For reference, here's the check_target_and_diag() routine:
The following sums up its computations:
- the return value is prepared in d7, as 0x3 (two lowest bit already set): the routine will unset those bits only if it finds a free cell at the target and diagonal locations;
- at 0x469C: the routine applies the desired transformation (rotation) to the current coordinates of the main puyo, by selecting the correct offset to apply to each axis from a predefined memory location (16 bytes starting at word_4706). This gives absolute (x,y) coordinates in the board reference plane for the target cell after rotation;
- the routine successively checks the target cell (from 0x46AC) then the diagonal cell (from 0x46D6);
- for each cell, if the desired coordinates are beyond the board limits, the game skips the check and returns a value indicating an obstructed cell. Indeed, no valid memory location is allocated for out of bounds cells;
- if the coordinates are actually valid, the routine calls compute_offset() to get the desired cell in-memory address, then checks its content and unsets the corresponding bit if the cell was empty.
From now on, the rotation routine determined whether the intended destination cell for the rotating puyo was empty or not. As stated earlier, if it was empty, the routine skips to the end. For the sake of the explanation, we will now consider what happens when the target cell is obstructing.
Current row check
We now know the destination cell is obstructed by either a puyo or the board edges.
Before going on to check if there's room to push back the pair, at 0x62EC the game checks whether the player's main puyo is currently in any of the ghost rows (y>=2?).
If that's the case, the game then determines (at 0x62F4) what rotation the player wants to achieve:
- rotating to a sideway position (left or right of the main puyo, transform IDs 1 and 3) is allowed and the routine will go on;
- rotating to an upright position (at the top or below the main puyo, transform IDs 0 and 2) is not allowed and the routine promptly exits.
This prevents the player from rotating a piece currently in the ghost rows, if the rotation would have resulted in pushing the pair upward (i.e. if the cell immediately below was obstructing the rotation).
Opposite cell check
After checking a "corner case" with the ghost rows, the routine tackles the opposite cell from the target cell, relative to the main puyo. This helps the game determine if it can push the player's pair in that direction: at the bottom of the board, the game will push the pair upwards, while it will push it sideways when rotating against a wall or a column.
Another clever math is done on the target transformation ID: xoring it's value with "2" gives a new and virtual transformation ID, which translates to the opposite cell. check_target_and_diag() is called again to check if it is obstructed or not. While this should never happen when targeting position #2 (bottom, as the upper part of the column should be empty), the check is done anyway. This explicitly handles the case where the player is stuck between two columns and tries to rotate his pair: when pushing the button first, the target cell for the 90° rotation is occupied, as well as its opposite counterpart.
If the opposite cell is empty (0x6304), the routine skips to the push back section. On the contrary, the double-tap mechanism kicks in.
Double-rotation
Another xor (at 0x630A) of the intended rotation ID (with the value "3"), accounts for the two rotations needed when flipping a pair over.
For each individual pair, a rotation attempts counter is initialized at 0. It is only incremented when the player tries to rotate his pair while stuck between columns (at 0x6314).
If the counter holds an odd value after being incremented, the routine ends abruptly, discarding the rotation but keeping in "mind" that such a rotation attempt has occurred.
If the counter holds an even value, the rotation is allowed to go through.
A yet-unknown player-specific game option triggers a different behavior at every attempt, effectively requiring three consecutive button inputs to allow the rotation to go through.
The input button only matters at the even counter values, and will only then determine the rotation direction.
Finally, if the double tap occurred, the target rotation ID is adjusted according to the rotation direction, if needed (only adjusted if the input was a counter-clockwise rotation).
Push back
From now on, nothing will cancel the rotation.
The routine finally proceeds to pushing back the pair according to the previous outcome:
- if the opposite cell is free, the pair is pushed back there (sideways or upwards);
- if the opposite cell is full, the pair is stuck between two columns; after double tapping the button:
- a rotation pushes the pair's main puyo upwards, with the slave puyo taking its place at the bottom;
- or the slave puyo ends up at the top with the main puyo being pushed down by one cell.
The transformation applied to the main puyo's coordinates is stored beginning at 0x6384 (couples of x,y word values), and can be seen in the capture below:
At 0x635C, the routine explicitly sets the puyo's position through its current cell at a predetermined value just above the middle of its height (0x7FFE, while the middle is at 0x8000 and the total height is 0x10000).
Since the game does not carry the old position over after pushing the pair upwards or downwards, it allows for input tricks that will exploit it and keep a pair at the same cell indefinitely when stuck between two columns, or when the bottom cells are obstructed. On the contrary, the sideway motions will carry the old offset over, hence not resetting the delay before the pair carries over to the next cell. This could be exploited to skip some animations and/or lockouts.
Rotation acknowledgement
The end of the routine (at 0x6362) acknowledges the rotation by:
- resetting the double-tap counter to its closest even value;
- saving the new rotation/transformation ID to the memory structure (0x6372);
- saving current angle step (0x636E) and target angle step (0x6378) for the sprite animation;
- setting the rotation animation step "speed" (0x637C): this value will be added to the current angle step until it reaches the target angle. By default, it is incremented by 8, or 16 if a double-rotation occurred.
Pseudo-code algorithm
The routine can be summed up with the following pseudo-code. It may help understand flaws and use them for input trickery (discussed further below).
function rotation_start() { get_player_gamepad_type; read_gamepad_input; if(both_cc_and_ccw_buttons) exit; if(is_empty(target)) goto acknowledge; if(current_row < 2) if(target_cell == bottom || target_cell == top) exit; if(is_empty(opposite)) goto pushback; rotation_counter_attempts++; if(rotation_counter_attempts % 2 == 0) goto double_rotation; if(rotation_counter_attempts == 18) if(is_set(player_bit)) { rotation_counter_attempts = 17; exit; } double_rotation: set_double_rotation_transform_id; // instead of normal rotation which keeps the original target transformation ID pushback: shift_main_puyo_coordinates; if(y-axis-shift != 0) reset_in-cell_vertical_offset; acknowledge: reset_double_rotation_counter; write_new_rotation_id; prepare_sprite_animation; }
Possible tricks
Due to how the push-back mechanism works, it is possible to:
- keep the falling pair at a specific position indefinitely, when stuck between columns by double-tapping rotation buttons rapidly (before the current battle drop speed increases past soft drop speed);
- keep the falling pair hovering at the bottom of the board or over other puyos by rapidly rotating the pair back and forth (granted this double rotation occurs within an 8 frame delay);
- time a rotation to skip the bouncing animation occurring when placing a puyo, because it is triggered before the puyo actually reaches the bottom of its current cell;
It is not possible to ghost puyos by pushing them back up to the 14th row, as the routine will not allow rotating the puyo to the upright position if the cell below on the 12th row is obstruced. However, it is possible to skip over columns full to the 12th row in the horizontal position, to then rotate and reach a free cell on the 12th row, as depicted below (the main puyo being the red one):
The last rotation has to be performed before the pair reaches the bottom of its current row. In all, from the rotation putting the pair in the horizontal position, there's an 8-frame delay at 2P normal drop speed during which the rotation is possible before the pair is locked and split.