Here a currently (i.e. in V1.43) solvable level
Bow before the Holy Cow made by Nepsimolot (level is attached below):
(http://i.imgur.com/02hG3yO.png)
Problems
Currently there are two main problems:
1) Assume a basher ends his stroke when still 9-14 pixels before the steel area. Whether he will turn around depends on whether there is still removable terrain in front of him or not. If there is only air, he will turn around, which is totally unexpected, especially if there is a gap between him and the steel.
2) If there is a gap between the basher and the steel, one can often succeed in removing the terrain such that all further lemmings will fall down the gap, but the basher turns around. This is a rather weird combination that separates a lemming from the crowd while still profiting from the original function of the basher.
Discussion of the Basher-Steel Test map
The number in front of each hatch gives the distance of the lemming to the steel piece, when he is
- Hatch 1: This shows the minimal distance (= 5 pixels) where one can still assign bashers before a solid steel block, when there is no solid terrain in front of the lemming.
- Hatch 2: This shows the maximal distance (= 14 pixels) where a basher still turns around, when there is no solid terrain in front of him.
- Hatch 3: This shows the minimal distance (= 15 pixels) where the basher continues walking, when there is no solid terrain in front of him.
- Hatch 4: This shows the minimal distance (= 5 pixels) where one can still assign basher before a solid steel block, when there is solid terrain in front of the lemming.
- Hatch 5: This shows the maximal distance (= 8 pixels) where a basher still turns around, when there is solid terrain in front of him.
- Hatch 6: This shows the minimal distance (= 9 pixels) where the basher continues walking, when there is solid terrain in front of him.
- Hatch 7: This shows that if 8 terrain pixels are bashable directly in front of the lemming, then he will behave like there is terrain all the way to the steel.
- Hatch 8: This shows that if only 7 terrain pixels are bashable directly in front of the lemming, he will behave like there is only air in front of him.
- Hatch 9: This shows that if there is only a 3-pixel gap in front of the steel, there is no way the basher can fall into this gap.
- Hatch 10: This shows that if there is a 4-pixel gap in front of the steel, there is a way that the basher can fall into this gap. In this case the basher has to be assigned pixel-precise.
- Hatch 11: This shows that if there is a 4-pixel gap in front of the steel, the lemming may as well turn around before reaching the gap.
- Hatch 12: This shows that if there is a 7-pixel gap in front of the steel, the lemming may still turn around before reaching the gap. However he must be assigned pixel-precise.
- Hatch 13: This shows that if there is a 7-pixel gap in front of the steel, the lemming will usually fall into the gap.
- Hatch 14: This shows that if there is a 8-pixel gap in front of the steel, the lemming will always fall into the gap.
- Hatch 15: If the steel area is only 1 pixel wide, then one can assign the basher 2 pixels in front of the steel. However the lemming will not turn around! The same behavior also appears when assigning the basher directly in fronof the steel wall.
- Hatch 16: If the steel area is only 1 pixel wide, then one can assign the basher 7 pixels in front of the steel and he will still not turn around.
- Hatch 17: If the steel area is only 1 pixel wide, then one can assign the basher 8 pixels in front of the steel, but now he will turn around.
So when there is a 1-pixel wide steel area, we have the following behavior:
- Assign on distance 4: Not possible
- Assign on distance 8, 9, 10, 11, 12, 13 and 14: Possible and lemming turns around
- Assign on distance 1, 2, 3, 5, 6, 7 and >=15: Possible and lemming continues in same direction
Proposed mechanics changes
Do only one steel check: If the lemming would move in the current frame into steel and he cannot step onto the steel, then cancel the basher and turn him around.
This way the basher only stops directly in front of the steel (or when he finds gaps at the ground). The change should neither make levels impossible nor introduce new backroutes. It will however break some replays where bashers hit steel.
Solution to "Bow before the Holy Cow"
1) Bash at the very edge of the starting platform. This basher will fall down, because there is still some terrain left of the vertical bar. 1a) Further lemmings bashing at this position will not turn around, because they will still see the remaining terrain.
2) Turn one of the lemming at the bottom into a climber.
3) When the climber reaches the basher-gap in the vertical bar, let him bash away the remaining terrain. He will turn around, but this is completely irrelevant.
4) Now that there is only air in fron of the lemming on the original platform, let these lemmings bash at the very edge. Now they suddenly will turn around.
It might be a while before I do any physics changes, for reasons I've discussed before. I do like your suggested solution here. The one question I have is - following on from this, it would make sense to also change the assignment rule for basher. What would be a good rule to use here? Disallow the assignment if he would encounter steel on his first bash makes the most sense - it is possible (I'm pretty sure) to assign a basher, with no terrain between him and steel, such that he doesn't hit the steel on his first bash, but does detect "there's terrain ahead" and keep bashing, therefore hitting the steel on the second bash.
One thing that I don't think this suggested rule covers is a situation like in the attached image. For reference, the lower-left steel block is exactly sitting on the wood, then going clockwise from there, each block is one pixel higher. If I'm understanding your rule correctly, every lemming except the bottom-left one should be able to successfully bash past the steel. This would feel very weird. Under current NL rules, only the bottom-right lemming can bash under. It's debatable whether this is really much more logical than all but the bottom-left being able to.
Let's use a test version rather than theoretical discussion to examine how well this new suggestion works.
https://www.dropbox.com/s/bymx5vu7utk0qnt/NeoLemmix_V1.44n_1603151825.exe
Same applies as with the last test version I posted - you can ignore the warnings when loading a pack, as long as it doesn't use any of the to-be-removed-very-soon features (ie: gimmicks, secret level triggers, two-way teleporters, single-object teleporters).
I think it needs some further work (in particular: I have not made any change to the checks upon assigning a basher; only to those performed for a lemming already bashing), but I do see this as the start of a viable overhaul of the basher physics.
Basher behaviour in test versionActual code
You can get the full source code for this version at BitBucket: https://bitbucket.org/namida42/neolemmixplayer/branch/basher-test
function TLemmingGame.HandleBashing(L: TLemming): Boolean;
var
n, x, y, dy{, x1, y1}, Index: Integer;
fa: Boolean;
FrontObj: Byte;
begin
Result := False;
with L do
begin
index := lemFrame;
if index >= 16 then
Dec(index, 16);
if (11 <= index) and (index <= 15) then
begin
Inc(LemX, LemDx);
// check 3 pixels above the new position
if (HasPixelAt(LemX, LemY)) then
begin
dy := 0;
while (dy <= 3) do
begin
if (HasPixelAt(LemX, LemY - dy)) and not (HasPixelAt(LemX, LemY - dy - 1)) then
begin
LemY := LemY - dy;
break;
end else if (dy = 3) and (HasPixelAt(LemX, LemY - dy)) then
begin
Dec(LemX, LemDx);
if (ReadSpecialMap(LemX + LemDx, LemY - dy - 1) = DOM_STEEL)
or (((ReadSpecialMap(LemX + LemDx, LemY - dy - 1) = DOM_ONEWAYDOWN)
or ((ReadSpecialMap(LemX + LemDx, LemY - dy - 1) = DOM_ONEWAYLEFT) and (lemdx = 1))
or ((ReadSpecialMap(LemX + LemDx, LemY - dy - 1) = DOM_ONEWAYRIGHT) and (lemdx = -1)))) then
begin
if (ReadSpecialMap(LemX + LemDx, LemY - dy - 1) = DOM_STEEL) then
CueSoundEffect(SFX_HITS_STEEL);
Transition(L, baWalking, TRUE);
end;
Result := true;
exit;
end;
Inc(dy);
end;
end;
// check 3 pixels below the new position
dy := 0;
while (dy < 3) and (HasPixelAt_ClipY(LemX, LemY, dy) = FALSE) do
begin
Inc(dy);
Inc(LemY);
end;
if dy = 3 then
begin
if HasPixelAt_ClipY(LemX, LemY, dy) then
Transition(L, baWalking)
else
Transition(L, baFalling);
end;
Result := True;
Exit;
end
else begin
if (2 <= index) and (index <= 5) then
begin
// frame 2..5 and 18..21 or used for masking
ApplyBashingMask(L, index - 2);
// special treatment frame 5 (see txt)
if (LemFrame = 5) or (LemFrame = 21) then
begin
n := 0;
x := LemX + lemdx * 8;
y := LemY - 6;
fa := false;
//
while (n < 7)
and (HasPixelAt(x,y) = FALSE)
and (HasPixelAt(x,y+1) = FALSE) do
begin
if (HasPixelAt(x, y)) or (HasPixelAt(x, y+1)) then fa := true;
Inc(n);
Inc(x, LemDx);
end;
if (n = 7) then
begin
if HasPixelAt(LemX, LemY) then
Transition(L, baWalking, fa)
else
Transition(L, baFalling, fa);
end;
end;
end;
end;
end; // with
end;
Human Friendly
Disclaimer: This doesn't include code that will never trigger, and some parts are simplified. If there is a discrepancy between the two versions - trust the Delphi code, not the Human Friendly code, for obvious reasons.
Firstly, if the current animation frame is above 16, subtract 16 from it for the purpose of this snippet. This essentially means frame0 and frame16 behave alike, frame1 and frame17 behave alike, etc.
Check if the frame number is in the range of 11 to 15, inclusive. If it is:
> Move one pixel forward.
> Check for heights (0, 1, 2, 3) above lemming's position for a pixel that is solid, but the one above it is not.
> If no suitable pixel was found:
--> Undo the one pixel forward move.
--> Check if the last pixel checked before (now one pixel in front of the lemming and 3 pixels above) is steel or a wrong one way. If it is, become a walker and turn around (also play a sound if it's steel).
--> Either way, stop any further processing for this lemming.
> If a suitable pixel was found (in the 0, 1, 2, 3 heights check):
--> Move the lemming to that height (may be the height it's already at, due to 0 being checked).
--> Check for heights (0, 1, 2) below lemming's position for a pixel that is solid. If one is found, move the lemming to that height (again, may and usually will be the height its already at), and stop any further processing for this lemming.
--> If the previous step did not find any pixel, become a faller.
Check if the frame number is in the range of 2 to 5, inclusive. If it is:
> Apply the appropriate frame of the basher mask for the lemming's current frame.
> Check if the frame number is exactly 5. If it is:
--> Check at distances (8 .. 14) inclusive in front of the lemming, at heights of both 5 and 6 pixels above the lemming. If not a single solid pixel is found, become a walker (or a faller, if the terrain underneath the lemming has been removed in the meantime).
Brokenness TestingTo get an idea of how much this is likely to break levels / replays, I decided to run the mass replay checker on Lemmings Plus I & II using this test version (I was too lazy to check the rest). Lemmings Plus I had 4 broken replays (
Medi 11,
Psycho 3,
Psycho 20 and
Psycho 28), while Lemmings Plus II had 3 broken replays (
Cunning 11,
Genius 3 and
Genius 6). So - while we'll have to see how it goes when this change is further developed - it looks like the impact is going to be quite minor.
Experimental version results:
Reunion + Pimolems: 10 broken replays total (5 in each pack).
Together with the ~ 4 from the cloner/miner and prob a few from my single stuff fixing all of this is pretty replay friendly overall.
If both problems are addressed here: Going forward unril right before the wall + not bashing under the 1-3 pixel high steel as in namida's example, this should be a good fix :)
Potential Fix Version 2! :)
https://www.dropbox.com/s/khj0qmap837a18j/NeoLemmix_V1.44n_1603160157.exe?dl=0
Basher behaviour in NEW test version - Changes are bolded in the human-friendly "code". :)
Actual code
You can get the full source code for this version at BitBucket: https://bitbucket.org/namida42/neolemmixplayer/branch/basher-test
function TLemmingGame.HandleBashing(L: TLemming): Boolean;
var
n, x, y, dy{, x1, y1}, Index: Integer;
fa: Boolean;
FrontObj: Byte;
begin
Result := False;
with L do
begin
index := lemFrame;
if index >= 16 then
Dec(index, 16);
if (11 <= index) and (index <= 15) then
begin
Inc(LemX, LemDx);
// check for steel or wrong one-ways between 1 and 5 pixels above new position
dy := 1;
while (dy <= 5) do
begin
if (ReadSpecialMap(LemX, LemY - dy) in [DOM_STEEL, DOM_ONEWAYDOWN])
or ((ReadSpecialMap(LemX, LemY - dy) = DOM_ONEWAYLEFT) and (LemDx = 1))
or ((ReadSpecialMap(LemX, LemY - dy) = DOM_ONEWAYRIGHT) and (LemDx = -1)) then
begin
if ReadSpecialMap(LemX, LemY - dy) = DOM_STEEL then
CueSoundEffect(SFX_HITS_STEEL);
Dec(LemX, LemDx);
Transition(L, baWalking, true);
Result := true;
exit;
end;
Inc(dy);
end;
// check 3 pixels above the new position
if (HasPixelAt(LemX, LemY)) then
begin
dy := 0;
while (dy <= 3) do
begin
if (HasPixelAt(LemX, LemY - dy)) and not (HasPixelAt(LemX, LemY - dy - 1)) then
begin
LemY := LemY - dy;
break;
end else if (dy = 3) and (HasPixelAt(LemX, LemY - dy)) then
begin
Dec(LemX, LemDx);
{if (ReadSpecialMap(LemX + LemDx, LemY - dy - 1) = DOM_STEEL)
or (((ReadSpecialMap(LemX + LemDx, LemY - dy - 1) = DOM_ONEWAYDOWN)
or ((ReadSpecialMap(LemX + LemDx, LemY - dy - 1) = DOM_ONEWAYLEFT) and (lemdx = 1))
or ((ReadSpecialMap(LemX + LemDx, LemY - dy - 1) = DOM_ONEWAYRIGHT) and (lemdx = -1)))) then
begin
if (ReadSpecialMap(LemX + LemDx, LemY - dy - 1) = DOM_STEEL) then
CueSoundEffect(SFX_HITS_STEEL);
Transition(L, baWalking, TRUE);
end;}
Result := true;
exit;
end;
Inc(dy);
end;
end;
// check 3 pixels below the new position
dy := 0;
while (dy < 3) and (HasPixelAt_ClipY(LemX, LemY, dy) = FALSE) do
begin
Inc(dy);
Inc(LemY);
end;
if dy = 3 then
begin
if HasPixelAt_ClipY(LemX, LemY, dy) then
Transition(L, baWalking)
else
Transition(L, baFalling);
end;
Result := True;
Exit;
end
else begin
if (2 <= index) and (index <= 5) then
begin
// frame 2..5 and 18..21 or used for masking
ApplyBashingMask(L, index - 2);
// special treatment frame 5 (see txt)
if (LemFrame = 5) or (LemFrame = 21) then
begin
n := 0;
x := LemX + lemdx * 8;
y := LemY - 6;
fa := false;
//
while (n < 7)
and (HasPixelAt(x,y) = FALSE)
and (HasPixelAt(x,y+1) = FALSE) do
begin
if (HasPixelAt(x, y)) or (HasPixelAt(x, y+1)) then fa := true;
Inc(n);
Inc(x, LemDx);
end;
if (n = 7) then
begin
if HasPixelAt(LemX, LemY) then
Transition(L, baWalking, fa)
else
Transition(L, baFalling, fa);
end;
end;
end;
end;
end; // with
end;
Human Friendly
Disclaimer: This doesn't include code that will never trigger, and some parts are simplified. If there is a discrepancy between the two versions - trust the Delphi code, not the Human Friendly code, for obvious reasons.
Firstly, if the current animation frame is above 16, subtract 16 from it for the purpose of this snippet. This essentially means frame0 and frame16 behave alike, frame1 and frame17 behave alike, etc.
Check if the frame number is in the range of 11 to 15, inclusive. If it is:
> Move one pixel forward.
> Check for heights (3, 4, 5) above lemming's position for a pixel that is steel or a wrong one-way. If such a pixel was found:
--> If the pixel was steel, play a sound effect.
--> Undo the one pixel forward move.
--> Become a walker and turn around.
> Check for heights (0, 1, 2, 3) above lemming's position for a pixel that is solid, but the one above it is not.
> If no suitable pixel was found:
--> Undo the one pixel forward move.
--> (Some steel checking code was removed here)
--> Stop any further processing for this lemming.
> If a suitable pixel was found (in the 0, 1, 2, 3 heights check):
--> Move the lemming to that height (may be the height it's already at, due to 0 being checked).
--> Check for heights (0, 1, 2) below lemming's position for a pixel that is solid. If one is found, move the lemming to that height (again, may and usually will be the height its already at), and stop any further processing for this lemming.
--> If the previous step did not find any pixel, become a faller.
Check if the frame number is in the range of 2 to 5, inclusive. If it is:
> Apply the appropriate frame of the basher mask for the lemming's current frame.
> Check if the frame number is exactly 5. If it is:
--> Check at distances (8 .. 14) inclusive in front of the lemming, at heights of both 5 and 6 pixels above the lemming. If not a single solid pixel is found, become a walker (or a faller, if the terrain underneath the lemming has been removed in the meantime).
Version 2:
Reunion: 6 broken replays
1 level is actually impossible ("The Lemming Tank Suicide Train") due to a basher not stopping anymore ----> simply terrain altering or giving 1 more builder will do the trick
Pimolems: 7 broken replays
Quote1 level is actually impossible ("The Lemming Tank Suicide Train") due to a basher not stopping anymore ----> simply terrain altering or giving 1 more builder will do the trick
Can you send me a replay showing what gives rise to this? There should be no difference (outside of very precise cases most likely set up for testing purposes) as to what overhead steel a lemming can or can't bash under; the only difference is that he might bash (or move) a tad further before stopping.
Attached to this post!
Ah - I see now. I was completley misunderstanding what you were referring to. Though I'm somewhat surprised that that would break in V2 but not V1. o_O
I'm not really sure if we should change the behaviour in this situation, but I'm open to discussion on it.
Quote from: namida on March 15, 2016, 02:29:39 PM
Ah - I see now. I was completley misunderstanding what you were referring to. Though I'm somewhat surprised that that would break in V2 but not V1. o_O
I'm not really sure if we should change the behaviour in this situation, but I'm open to discussion on it.
I am not sure if I simply overlooked it breaking in V1. In this specific situation i don't see the need for a change, but fixing this level is only 5min of work max for me. If other people have bigger problems then maybe it could be bad.
------> we need more test results!
I recommend reading the actual code instead of the human friendly version. The human friendly version contains a few more bugs, that the code does not have.
Replay test for NepsterLems: 162 replays working, 4 failing. :lix-grin:
@IchoTolot: Could you please explain what the problem is, or at least give us the exact position of
Lemming Suicide Train in your level pack. I prefer not to go through all of your level pack in order to find the level. ;)
@namida: I find the code rather hard to read, because the lemming gets moved around all the time. Why not deciding on the final position first and then moving the lemming there at the very end?
Anyway, given your code (using experimental version 2) I would have expected that a basher falling down has its falling distance reduced by 3, because it got moved down already 3 pixels during the basher function. However experiments show that being a basher doesn't matter whether a lemmig splats or not. Where is my mistake?
Still if the basher is a floater, he opens the parachute one pixel higher than usual lemmings falling down. So there
is something not completely right with the faller transition.
QuoteI think it needs some further work (in particular: I have not made any change to the checks upon assigning a basher; only to those performed for a lemming already bashing), but I do see this as the start of a viable overhaul of the basher physics.
Do we even need the steel check on assignment at all, if we use the new checks during the basher animation?
Quote from: Nepster on March 15, 2016, 08:49:24 PM
I recommend reading the actual code instead of the human friendly version. The human friendly version contains a few more bugs, that the code does not have.
Replay test for NepsterLems: 162 replays working, 4 failing. :lix-grin:
@IchoTolot: Could you please explain what the problem is, or at least give us the exact position of Lemming Suicide Train in your level pack. I prefer not to go through all of your level pack in order to find the level. ;)
@namida: I find the code rather hard to read, because the lemming gets moved around all the time. Why not deciding on the final position first and then moving the lemming there at the very end?
Anyway, given your code (using experimental version 2) I would have expected that a basher falling down has its falling distance reduced by 3, because it got moved down already 3 pixels during the basher function. However experiments show that being a basher doesn't matter whether a lemmig splats or not. Where is my mistake?
Still if the basher is a floater, he opens the parachute one pixel higher than usual lemmings falling down. So there is something not completely right with the faller transition.
QuoteI think it needs some further work (in particular: I have not made any change to the checks upon assigning a basher; only to those performed for a lemming already bashing), but I do see this as the start of a viable overhaul of the basher physics.
Do we even need the steel check on assignment at all, if we use the new checks during the basher animation?
I've attached a replay to one of my ealier posts. The position is rank 3 lvl 15. There is only 1 basher in this level and I've made a tinsy mistake in judging the situation :-[ It is still possible! The basher gone through the whole way before --- hitting the steel and therefore messed up the whole builder situation.
Everything is in order just a broken replay!
Just looked a bit more at the experimental code and found some more bugs:
- Bashers can now climb much steeper inclines (about three times as steep)
- If a basher moved up or down, the steel checks are at the wrong height
But because I failed to really unterstand the code and the parts that I managed to understand do weird stuff, I completely rewrote the basher function and hopefully fixed the bugs above. There are still some parts that
should be separate functions, but hopefully it is now slightly easier to read:
TLemmingGame.HandleBashing
function TLemmingGame.HandleBashing(L: TLemming): Boolean;
var
LemDy, n: Integer;
ContinueWork: Boolean;
begin
with L do
begin
if LemFrame >= 16 then
Dec(LemFrame, 16);
// Remove terrain
if (LemFrame in [2, 3, 4, 5]) then
ApplyBashingMask(L, LemFrame - 2);
// special treatment frame 5 (see txt)
// This probably means: Check for enough terrain to continue working
if LemFrame = 5 then
begin
ContinueWork := False;
For n := 8 to 14 do
begin
If HasPixelAt(LemX + n*LemDx, LemY - 6) then ContinueWork := True;
If HasPixelAt(LemX + n*LemDx, LemY - 5) then ContinueWork := True;
end;
if ContinueWork = False then
begin
if HasPixelAt(LemX, LemY) then
Transition(L, baWalking)
else
Transition(L, baFalling);
end;
end;
if (LemFrame in [11, 12, 13, 14, 15]) then
begin
Inc(LemX, LemDx);
// Getting new LemY position
LemDy := 0;
if HasPixelAt(LemX, LemY) then
begin
while (HasPixelAt(LemX, LemY + LemDy - 1)) and (LemDy > -3) do
Dec(LemDy);
end
else
begin
Inc(LemDy);
while (HasPixelAt(LemX, LemY + LemDy) = False) and (LemDy < 4) do
Inc(LemDy);
end;
// Moving in new position, if not hitting steel
if LemDy = 4 then
begin
// Transition to faller
Inc(LemY, 3); // IS 3 PIXELS THE SAME AS FOR WALKER->FALLER??
Transition(L, baFalling);
end
else if LemDy = 3 then
begin
// Move three pixels down and transition to walker
Inc(LemY, LemDy);
Transition(L, baWalking);
end
else if (LemDy in [0, 1, 2]) then
begin
// Move no, one or two pixels down, if not hitting steel
if BasherIndestructibleCheck(LemX, LemY + LemDy, LemDx) then
BasherTurn(L, HasSteelAt(LemX, LemY + LemDy - 4));
else
Inc(LemY, LemDy);
end;
end
else if LemDy = -1 then
begin
// Move one pixel up, if not hitting steel and not too much terrain
if BasherIndestructibleCheck(LemX, LemY + LemDy, LemDx) then
BasherTurn(L, HasSteelAt(LemX, LemY + LemDy - 4));
else if ( (HasPixelAt(LemX + LemDx, LemY + LemDy - 1) = False)
and (HasPixelAt(LemX + LemDx, LemY + LemDy))
and (HasPixelAt(LemX + 2*LemDx, LemY + LemDy)
and (HasPixelAt(LemX + 2*LemDx, LemY + LemDy - 1))
and (HasPixelAt(LemX + 2*LemDx, LemY + LemDy - 2))
)
BasherTurn(L, HasSteelAt(LemX + LemDx, LemY + LemDy));
else if ( (HasPixelAt(LemX + LemDx, LemY + LemDy - 2) = False)
and (HasPixelAt(LemX + LemDx, LemY + LemDy))
and (HasPixelAt(LemX + LemDx, LemY + LemDy - 1))
and (HasPixelAt(LemX + 2*LemDx, LemY + LemDy - 1))
and (HasPixelAt(LemX + 2*LemDx, LemY + LemDy - 2))
)
BasherTurn(L, HasSteelAt(LemX + LemDx, LemY + LemDy));
else if ( (HasPixelAt(LemX + LemDx, LemY + LemDy - 2))
and (HasPixelAt(LemX + LemDx, LemY + LemDy - 1))
and (HasPixelAt(LemX + LemDx, LemY + LemDy))
)
BasherTurn(L, HasSteelAt(LemX + LemDx, LemY + LemDy));
else
Inc(LemY, LemDy); // Lem may move up
end;
end
else if LemDy = -2 then
begin
// Move two pixels up, if not hitting steel and not too much terrain
if BasherIndestructibleCheck(LemX, LemY + LemDy, LemDx) then
BasherTurn(L, HasSteelAt(LemX, LemY + LemDy - 4));
else if ( (HasPixelAt(LemX + LemDx, LemY + LemDy) = False)
and (HasPixelAt(LemX + LemDx, LemY + LemDy + 1))
and (HasPixelAt(LemX + 2*LemDx, LemY + LemDy + 2))
and (HasPixelAt(LemX + 2*LemDx, LemY + LemDy + 1))
and (HasPixelAt(LemX + 2*LemDx, LemY + LemDy))
)
BasherTurn(L, HasSteelAt(LemX + LemDx, LemY + LemDy + 1));
else if ( (HasPixelAt(LemX + LemDx, LemY + LemDy - 1) = False)
and (HasPixelAt(LemX + LemDx, LemY + LemDy))
and (HasPixelAt(LemX + 2*LemDx, LemY + LemDy))
and (HasPixelAt(LemX + 2*LemDx, LemY + LemDy - 1))
)
BasherTurn(L, HasSteelAt(LemX + LemDx, LemY + LemDy));
else
Inc(LemY, LemDy); // Lem may move up
end;
end
else if LemDy = -3 then
begin
// Either stall or turn if there is steel
if BasherIndestructibleCheck(LemX, LemY, LemDx) then
BasherTurn(L, (HasSteelAt(LemX, LemY - 3) or HasSteelAt(LemX, LemY - 4) or HasSteelAt(LemX, LemY - 5)));
else
Dec(LemX, LemDx);
end;
end;
end;
Result := True;
end;
If calls helper functions:
BasherTurn
function BasherTurn(L: TLemming, SteelSound: Boolean): Boolean;
begin
// Turns basher around and transitions to walker
with L do
begin
Dec(LemX, LemDx);
Transition(L, baWalking);
end;
if SteelSound then CueSoundEffect(SFX_HITS_STEEL);
Return := True;
end;
BasherIndestructibleCheck
function BasherIndestructibleCheck(x: Integer, y: Integer, Direction: Integer): Boolean;
begin
// check for indestructible terrain 3, 4 and 5 pixels above (x, y)
Result := ( (HasIndestructibleAt(x, y - 3, Direction))
or (HasIndestructibleAt(x, y - 4, Direction))
or (HasIndestructibleAt(x, y - 5, Direction))
);
end;
HasIndestructibleAt and HasSteelAt
function HasIndestructibleAt(x: Integer, y: Integer, Direction: Integer): Boolean;
begin
// check for indestructible terrain at position (x, y)
Return := ( ( ReadSpecialMap(x, y) = DOM_STEEL)
or ( ReadSpecialMap(x, y) = DOM_ONEWAYDOWN)
or ((ReadSpecialMap(x, y) = DOM_ONEWAYLEFT) and (Direction = 1))
or ((ReadSpecialMap(x, y) = DOM_ONEWAYRIGHT) and (Direction = -1))
);
end;
function HasSteelAt(x: Integer, y: Integer): Boolean;
begin
// check for steel at position (x, y)
Return := (ReadSpecialMap(x, y) = DOM_STEEL);
end;
Warning: I don't have Delphi installed, so there are likely several syntax mistakes in the code above. Of course this implies as well, that I couldn't test that the rewritten code indeed replicates (most of) the original basher function.
Finally I noticed the following: If the lowest 5 pixels are air and from pixel 6 on there is steel, then the lemming will happily continue bashing forward without removing any terrain pixel at all. Do we want to keep this behavior?
Quote from: Nepster on March 16, 2016, 09:17:49 PMFinally I noticed the following: If the lowest 5 pixels are air and from pixel 6 on there is steel, then the lemming will happily continue bashing forward without removing any terrain pixel at all. Do we want to keep this behavior?
No, for consistency. Hanging on to steel like that is something the digger could do in L1, and this has been changed in NeoLemmix. Basher should follow suit.
Quote from: Proxima on March 16, 2016, 09:20:53 PM
Quote from: Nepster on March 16, 2016, 09:17:49 PMFinally I noticed the following: If the lowest 5 pixels are air and from pixel 6 on there is steel, then the lemming will happily continue bashing forward without removing any terrain pixel at all. Do we want to keep this behavior?
No, for consistency. Hanging on to steel like that is something the digger could do in L1, and this has been changed in NeoLemmix. Basher should follow suit.
Then replace the line
If HasPixelAt(LemX + n*LemDx, LemY - 6) then ContinueWork := True;
by
If (HasPixelAt(LemX + n*LemDx, LemY - 6) and (HasIndestructibleAt(LemX + n*LemDx, LemY - 6) = False)) then ContinueWork := True;
in the code above.
More technical background: in L1, steel areas are set up separately from the steel terrain pieces. Ignoring that point though, the steel checking is done such that it's actually the inverse that works in L1 and not exactly the setup given by Nepster: The lowest 4 pixels from ground level are not checked for steel, only the upper 4 pixels (and due to multiple-of-4 alignments in how steel areas are tracked, in some cases it can actually be up to lowest 7 pixels). So the lower portions of steel can be bashed away, which is typically considered a form of steel destruction glitch in L1. A particularly egregious form of that is ceiling steel-bashing (where the location the game actually looks for steel is off the level's top boundary, so even though the steel reaches the top boundary and likely was never intended to be bypassable with a basher, you still can by starting to bash high enough).
The actual terrain removal mask in L1 is 9 pixels tall IIRC, so if the lowest steel is located at that 9th row, it is not detected either. While this sound sort of bad, in practice it seems that (some?) people would rather have a slight margin of error and be okay with one or two rows of pixels at the top and/or bottom be ignored for steel checking.
L2 has more honest steel treatment in that steel pixels are never destructible. I forgot exactly how it checks for steel during bashing, but I believe there is some forgiveness so that something like the bottom row and 9th row (and perhaps more?) may be ignored when checking for steel. Ignore means they don't stop the basher, but they remain indestructible and are simply not removed as part of the bash stroke.
Anyway, the setup described by Nepster does sound very counterintuitive and probably should not be kept. (Having a margin of error in steel checking would not require maintaining such behavior--the margins are set up to be outside the zone that is checked to determine whether there is more ahead to bash.) In fact, it feels like the margin of error for steel may be too forgiving for such a setup?
QuoteAnyway, given your code (using experimental version 2) I would have expected that a basher falling down has its falling distance reduced by 3, because it got moved down already 3 pixels during the basher function. However experiments show that being a basher doesn't matter whether a lemmig splats or not. Where is my mistake?
TLemmingGame.Transition
if (aAction = baFalling) then
begin
LemFallen := -2;
if LemAction in [baWalking] then LemFallen := 0;
if LemAction in [baBashing] then LemFallen := -1;
if LemAction in [baMining, baDigging] then LemFallen := -3;
end;
This code handles setting the initial fall distance. I couldn't tell you off-hand
why these values work - they were derived a very long time ago, by trial-and-error rather than by analysing code. However, just in case, I tested it now and can confirm - both a walker or a basher, falling from a 64 pixel height, will splat. Either one falling from a 63 pixel height will not. So while the code may be a bit strange here, there is no inconsistency in the end result.
QuoteStill if the basher is a floater, he opens the parachute one pixel higher than usual lemmings falling down. So there is something not completely right with the faller transition.
This (which probably should be fixed) is because a walker is moved downwards by one pixel more than a basher is, before transitioning to a faller. Although the splat distance is, unlike L1, checked on a pixel-perfect basis; the timing of pulling out a floater is unchanged from L1. For a floater this generally won't make a huge difference (in some cases it may mean a walker arrives at the ground one frame sooner than a basher, even if both begun falling at the same time - this also holds true for regular fallers as well), but I could see this being significant when it comes to Gliders.
QuoteDo we even need the steel check on assignment at all, if we use the new checks during the basher animation?
I was wondering the same thing myself. However, as far as I recall, no one suggests changing the check-on-assign for the digger. People also seem to want the check-on-assign for miners to be made more strict. In light of this, making the basher check
less strict seems a bit strange.
QuoteJust looked a bit more at the experimental code and found some more bugs:
- Bashers can now climb much steeper inclines (about three times as steep)
- If a basher moved up or down, the steel checks are at the wrong height
The first one might be desirable for consistency (and at any rate, is a very unusual setup, though it might occur in the Sky set or by very well-timed use of Stoners, or perhaps even Stackers). The only reason the lemming couldn't do so before is because the steel check caused him to revert to a walker. In the absence of this - perhaps if the aforementioned stoner / stacker timing setup was used to create such a layout - the basher would have been able to do this too, I believe.
The second one I'll look into.
QuoteFinally I noticed the following: If the lowest 5 pixels are air and from pixel 6 on there is steel, then the lemming will happily continue bashing forward without removing any terrain pixel at all. Do we want to keep this behavior?
No, we do not. We should compare this to a digger - although steel (other than right underneath the lemming) won't
stop the lemming, they don't count as solid pixels for the terrain check.
In regards to your proposed code - I'm planning to overhaul the way the code is written in the near future, so I don't want to worry too much about improving the way the current code is laid out. Rather, my intention is - decide on and implement new physics by adjusting the current code, then when that's done, do the overhaul, at which point the improved-readability etc changes would also be made.
FWIW though, as far as I can (without actually testing it) tell that code is mostly fine, except for "Return :=" rather than "Result :=" in the first snippet. Although the new functions should probably be defined as private functions of TLemmingGame, rather than standalone ones.
Quote from: ccexplore on March 16, 2016, 11:13:50 PM
In fact, it feels like the margin of error for steel may be too forgiving for such a setup?
Interesting suggestion.
- Pixels 1 and 2: Should not be checked, because the basher may step upon them.
- Pixels 3, 4 and 5: Already checked.
- Pixels 6, 7: Might result in improved basher mechanics.
- Pixels 8, 9: Steel checks here might surprise some people: Why should lemmings turn around if their hair touches steel?
Quote from: namida on March 17, 2016, 02:31:11 AM
QuoteDo we even need the steel check on assignment at all, if we use the new checks during the basher animation?
I was wondering the same thing myself. However, as far as I recall, no one suggests changing the check-on-assign for the digger. People also seem to want the check-on-assign for miners to be made more strict. In light of this, making the basher check less strict seems a bit strange.
These checks are not really consistent anyway. The first question should be: What behavior do we actually want? Then I would implement the checks according to the answer to this question. If it turns out that we want miner to be more strict and basher less strict, then so be it.
But all of this is better discussed in the assignment check topic.
Quote from: namida on March 17, 2016, 02:31:11 AM
QuoteJust looked a bit more at the experimental code and found some more bugs:
- Bashers can now climb much steeper inclines (about three times as steep)
The first one might be desirable for consistency (and at any rate, is a very unusual setup, though it might occur in the Sky set or by very well-timed use of Stoners, or perhaps even Stackers). The only reason the lemming couldn't do so before is because the steel check caused him to revert to a walker. In the absence of this - perhaps if the aforementioned stoner / stacker timing setup was used to create such a layout - the basher would have been able to do this too, I believe.
You forget slopes made out of steel or wrong OWW. I seriously doubt that we want the behavior in the attached image.
Quote from: namida on March 17, 2016, 02:31:11 AM
In regards to your proposed code - I'm planning to overhaul the way the code is written in the near future, so I don't want to worry too much about improving the way the current code is laid out. Rather, my intention is - decide on and implement new physics by adjusting the current code, then when that's done, do the overhaul, at which point the improved-readability etc changes would also be made.
Sorry, but this does not make any sense. With the current mostly unreadable code, we can create a few test maps (which might be a good thing to do anyway), but nothing more. In particular we will certainly miss some bugs or glitches in the code. So after making the code readable, we have to do the same testing again and have subsequent physics changes which annoy all the level designers by breaking their replays yet again.
Moreover I will take any bet, that nobody can overhaul the code without introducing a few new subtle bugs or glitches.
So I see three options how to proceed:
1) I stop testing experimental releases and stop reporting physics bugs until the code got readable, because it is currently wasted time to do so.
2) Whenever you fix some bugs/glitches in some function, you overhaul this function and make it readable.
3) You outsource overhauling the game mechanics code, by giving this task to me. Prerequisites are that you don't plan changing major global design decisions for the code and that I get a free Delphi compiler from somewhere in order to test the mess I code.
What do you prefer?
I can't make any promises in regards to the lack of global changes. In particular, one that's likely to overlap with the mechanics code (almost by definition) is seperating the rendering and physics from each other; you've probably noticed that currently they're quite intertwined.
I get that the plan here seems strange, but by knowing exactly what I'm trying to implement, what information each skill needs, etc, when overhauling it, I can ultimately produce better code, even if the code remains a bit messier in the meantime. Until then, any changes should be made as local as possible to the specific action, and anything not explicitly decided on changing should be unchanged, so that we don't end up with unexpected accidental results.
I don't plan on making any release that includes these physics changes until both deciding on them, and the overhaul of how physics is implemented, are both done. So, unless authors are updating their replays for the experimental versions, it is not going to result in a case of double-breakage.
It is not wasted time to report the bugs, because the idea isn't "scratch everything, make a new set of physics". It's "firstly get the physics right, then tidy up the code for them". In particular (as you may have seen in IRC discussions if you follow the logs), I'm wanting to move much of the processing from TLemmingGame to TLemming, and implement objects that define the behaviours. This would, of course, require knowing what those behaviours are going to do. The alternative here is to do that now, and potentially end up with suboptimal code due to all the additions / removals / changes that occur when investigating physics.
That's all fine and good, but my two main concerns remain: If you do "firstly get the physics right, then tidy up the code for them", then
1) How do we know that we got the physics right, if the code is barely readable?
2) How do we know that we did not change the physics while tidying up the code?
1. "Barely readable" is perhaps an exaggeration. "Not as tidy and efficient as it could be" is more accurate. It's not so bad that bugs can't be spotted (I've already found several by looking through the code, that were never discovered in regular gameplay - the
assigning-skills-to-stonesploder glitch being an example of such), and I also wouldn't say I'm not going to tidy up little things as I go - more just not going out of my way to do so. If I see commented-out or never-used code, I'll probably remove it.
2. Test maps and replay checking. Which I do need to get around to making more of, I'm thinking I should probably make a graphic set specifically designed for test map purposes (ie: no emphasis at all on looking nice; the emphasis rather is on being able to put together the kind of structures that are useful for test maps, and designed so that where applicable, distances can be accurately determined visually without having to back-calculate them from the physics or check in a graphic editing program).
Yes, "barely readable" was an exaggeration. But let's have a look at the latest bugs I found:
- Bashers move up too steep slopes: Found via test map.
- Misplaced steel checks if basher steps up/down: Found via looking at the original code.
- Basher continues with steel at height 6: Only noticed after looking at my rewritten code.
- Basher turns if (LemX, LemY) is empty, but (LemX, LemY - n) is steel for n > 0: I noticed this already yesterday in the original code. However when I tested this, I got confused, because I mistakenly thought the lemming actually correctly transitions to a faller. So I spent one hour trying to understand where the code makes such a lemming a faller. I gave up and spent today again about half an hour, before testing it again and realizing my mistake. Lots and lots of time wasted!
PS: I believe that my rewritten code already fixes this bug, too ;).
Here is new experimental version of NeoLemmix, that in addition to namida's version, includes the following bugfixes:
- Basher turns at steep slopes (similar, but not 100% equivalent to the V1.43 behavior)
- Steel checks for turning around are always at correct height.
- Falling down has priority over turning, when both are applicable in one frame.
- Falling behavior is precisely the same as for walkers->fallers.
- Basher does no longer continue when steel is at height 6, but air elsewhere.
The fourth point breaks some of the replays, because now falling bashers are one pixel faster when falling down. So there is a 33% probability that they reach the ground one frame earlier, which may have the usual side effects that break replays.
For the NepsterLems replays approx. 5% of all replays fail due to this reason.
In regards to a point about what slopes bashers should or shouldn't be able to ascend - perhaps it's worthwhile getting some feedback.
I've attached a map with slopes of various steepness (note that for 3:2 and 2:3, I've done two versions of each to test for any inconsistencies between whether the 2px or the 1px section comes first). This map isnt' quite in shape for the test map pack yet, but does use the style from it. http://www.lemmingsforums.net/index.php?topic=2591.0
What I'd like is if everyone could tell me which bashers they would expect to get to the top, and which they'd expect to turn around. For record - the correct order to assign the bashers is top-to-bottom, left-to-right (the lemming positions are slightly offset from each other such that, if assigned in this order, one per frame, they'll all start at the same position).
For the record, the results on V1.43n-E, and the two recent test versions (the newer copy of mine, and Nepster's):
V1.43n-E
The lemmings with the 1:1 slope, the 1:2 slope, and the 2:3 slopes turn around. All others pass.
My test version
All lemmings pass.
Nepster's test version
The lemmings with the 1:1 slope, the 1:2 slope, and the 2:3 slopes turn around. All others pass. (Same as V1.43n-E.)
I do need to add some more inbetween angles (4:3 / 3:4 and perhaps 5:3 / 3:5).
As I said in IRC:
My vote goes to the Nepster/ 1.43 E way, as the basher in the namida test version goes waaaay too steep for my taste.
Also this way the upwards behavior stays consistant ;P
One problem I have noticed occurs in the intended solution replay to Dodgy 16 of Lemmings Plus III (both the game and the replay can be obtained here (http://www.lemmingsforums.net/index.php?topic=1922.0)).
Specifically, the basher who attempts to bash through the bottom-left part of the central structure.
In Nepster's first test version posted above, the basher, after moving downwards slightly, turns around. This is despite having one pixel remaining of his forwards movement, which has only a one pixel upwards step. Assign the basher a frame earlier, and the problem here becomes even more noticable. (Two frames earlier, and he doesn't encounter trouble.)
Nepster sent me a second test version which he hasn't uploaded here (EDIT: see post below). In this, although the basher doesn't stop at this position, he is still delayed, which seems equally strange.
Apart from this - and overall deciding what should be the limit - I do think Nepster's verison handles things very well, and the code is certianly much more readable.
In this case, the lemming has one pixel to step up ahead of him. If he were to take one step further forwards, the next would be a 2 pixel rise.
There's quite a lot of discussion going on on IRC in regards to both this specific situation and the basher behaviour in general. I recommend checking the logs (starting from about 18:04:15) (http://nordicbots.com/?id=73&net=quakenet&cid=84924&year=2016&month=3&day=19) for more details on what's being discussed.
Here is the version with the fix for the bug mentioned by namida, if anyone wants to test is.