ft2-clone

Fasttracker 2 clone
Log | Files | Refs | README | LICENSE

ft2_nibbles.c (24796B)


      1 #include <stdint.h>
      2 #include <stdio.h>
      3 #include <math.h> // round()
      4 #include "ft2_keyboard.h"
      5 #include "ft2_config.h"
      6 #include "ft2_video.h"
      7 #include "ft2_gui.h"
      8 #include "ft2_pattern_ed.h"
      9 #include "ft2_bmp.h"
     10 #include "ft2_tables.h"
     11 #include "ft2_structs.h"
     12 
     13 #define STAGES_BMP_WIDTH 530
     14 #define NI_MAXLEVEL 30
     15 
     16 static const char *NI_HelpText[] =
     17 {
     18 	"Player 1 uses cursor keys to control movement.",
     19 	"Player 2 uses the following keys:",
     20 	"",
     21 	"                  (W=Up)",
     22 	"  (A=Left) (S=Down) (D=Right)",
     23 	"",
     24 	"The \"Wrap\" option controls whether it's possible to walk through",
     25 	"the screen edges or not. Turn it on and use your brain to get",
     26 	"the maximum out of this feature.",
     27 	"The \"Surround\" option turns Nibbles into a completely different",
     28 	"game. Don't change this option during play! (you'll see why)",
     29 	"We wish you many hours of fun playing this game."
     30 };
     31 #define NIBBLES_HELP_LINES (sizeof (NI_HelpText) / sizeof (char *))
     32 
     33 typedef struct
     34 {
     35 	int16_t length;
     36 	uint8_t data[8];
     37 } nibblesBuffer_t;
     38 
     39 typedef struct
     40 {
     41 	uint8_t x, y;
     42 } nibblesCoord_t;
     43 
     44 static const char nibblesCheatCode1[] = "skip", nibblesCheatCode2[] = "triton";
     45 static char nibblesCheatBuffer[16];
     46 
     47 static const uint8_t NI_Speeds[4] = { 12, 8, 6, 4 };
     48 static bool NI_EternalLives;
     49 static uint8_t NI_CheatIndex, NI_CurSpeed, NI_CurTick60Hz, NI_CurSpeed60Hz, NI_Screen[51][23], NI_Level;
     50 static int16_t NI_P1Dir, NI_P2Dir, NI_P1Len, NI_P2Len, NI_Number, NI_NumberX, NI_NumberY, NI_P1NoClear, NI_P2NoClear;
     51 static uint16_t NI_P1Lives, NI_P2Lives;
     52 static int32_t NI_P1Score, NI_P2Score;
     53 static nibblesCoord_t NI_P1[256], NI_P2[256];
     54 static nibblesBuffer_t nibblesBuffer[2];
     55 
     56 /* Non-FT2 feature: Check if either the Desktop or Buttons palette color
     57 ** is so close to black that the player would have troubles seeing the walls
     58 ** when playing Nibbles.
     59 */
     60 // ------------------------------------------------------------------------
     61 // ------------------------------------------------------------------------
     62 static uint8_t rgb24ToLuminosity(uint32_t rgb24)
     63 {
     64 	const uint8_t R = RGB32_R(rgb24);
     65 	const uint8_t G = RGB32_G(rgb24);
     66 	const uint8_t B = RGB32_B(rgb24);
     67 
     68 	// get highest channel value
     69 	uint8_t hi = 0;
     70 	if (hi < R) hi = R;
     71 	if (hi < G) hi = G;
     72 	if (hi < B) hi = B;
     73 
     74 	// get lowest channel value
     75 	uint8_t lo = 255;
     76 	if (lo > R) lo = R;
     77 	if (lo > G) lo = G;
     78 	if (lo > B) lo = B;
     79 
     80 	return (hi + lo) >> 1; // 0..255
     81 }
     82 
     83 static bool wallColorsAreCloseToBlack(void)
     84 {
     85 #define LUMINOSITY_THRESHOLD 4
     86 
     87 	const uint8_t wallColor1L = rgb24ToLuminosity(video.palette[PAL_DESKTOP]);
     88 	const uint8_t wallColor2L = rgb24ToLuminosity(video.palette[PAL_BUTTONS]);
     89 
     90 	/* Since the rest of the wall colors are based on lower and higher
     91 	** contrast values from these two primary colors, we don't really
     92 	** need to check them all.
     93 	*/
     94 
     95 	if (wallColor1L <= LUMINOSITY_THRESHOLD || wallColor2L <= LUMINOSITY_THRESHOLD)
     96 		return true;
     97 
     98 	return false;
     99 }
    100 // ------------------------------------------------------------------------
    101 // ------------------------------------------------------------------------
    102 
    103 static void drawNibblesFoodNumber(int32_t xOut, int32_t yOut, int32_t number)
    104 {
    105 	if (number > 9)
    106 		return;
    107 
    108 	uint32_t *dstPtr = &video.frameBuffer[(yOut * SCREEN_W) + xOut];
    109 	uint8_t *srcPtr = &bmp.font8[number * FONT8_CHAR_W];
    110 
    111 	for (int32_t y = 0; y < FONT8_CHAR_H; y++, srcPtr += FONT8_WIDTH, dstPtr += SCREEN_W)
    112 	{
    113 		for (int32_t x = 0; x < FONT8_CHAR_W; x++)
    114 		{
    115 			if (srcPtr[x] != 0)
    116 				dstPtr[x] = video.palette[PAL_FORGRND];
    117 		}
    118 	}
    119 }
    120 
    121 static void redrawNibblesScreen(void)
    122 {
    123 	if (!editor.NI_Play)
    124 		return;
    125 
    126 	for (int16_t x = 0; x < 51; x++)
    127 	{
    128 		for (int16_t y = 0; y < 23; y++)
    129 		{
    130 			const int16_t xs = 152 + (x * 8);
    131 			const int16_t ys = 7 + (y * 7);
    132 
    133 			const uint8_t c = NI_Screen[x][y];
    134 			if (c < 16)
    135 			{
    136 				if (config.NI_Grid)
    137 				{
    138 					fillRect(xs + 0, ys + 0, 8 - 0, 7 - 0, PAL_BUTTON2);
    139 					fillRect(xs + 1, ys + 1, 8 - 1, 7 - 1, c);
    140 				}
    141 				else
    142 				{
    143 					fillRect(xs, ys, 8, 7, c);
    144 				}
    145 			}
    146 			else
    147 			{
    148 				drawNibblesFoodNumber(xs+2, ys, NI_Number);
    149 			}
    150 		}
    151 	}
    152 
    153 	// fix wrongly rendered grid
    154 	if (config.NI_Grid)
    155 	{
    156 		vLine(560,   7, 161, PAL_BUTTON2);
    157 		hLine(152, 168, 409, PAL_BUTTON2);
    158 	}
    159 	else
    160 	{
    161 		// if we turned grid off, clear lines
    162 		vLine(560,   7, 161, PAL_BCKGRND);
    163 		hLine(152, 168, 409, PAL_BCKGRND);
    164 	}
    165 }
    166 
    167 static void nibblesAddBuffer(int16_t bufNum, uint8_t value)
    168 {
    169 	nibblesBuffer_t *n = &nibblesBuffer[bufNum];
    170 	if (n->length < 8)
    171 	{
    172 		n->data[n->length] = value;
    173 		n->length++;
    174 	}
    175 }
    176 
    177 static bool nibblesBufferFull(int16_t bufNum)
    178 {
    179 	return (nibblesBuffer[bufNum].length > 0);
    180 }
    181 
    182 static int16_t nibblesGetBuffer(int16_t bufNum)
    183 {
    184 	nibblesBuffer_t *n = &nibblesBuffer[bufNum];
    185 	if (n->length > 0)
    186 	{
    187 		const int16_t dataOut = n->data[0];
    188 		memmove(&n->data[0], &n->data[1], 7);
    189 		n->length--;
    190 
    191 		return dataOut;
    192 	}
    193 
    194 	return -1;
    195 }
    196 
    197 static void nibblesGetLevel(int16_t levelNum)
    198 {
    199 	const int32_t readX = 1 + ((51+2) * (levelNum % 10));
    200 	const int32_t readY = 1 + ((23+2) * (levelNum / 10));
    201 
    202 	const uint8_t *stagePtr = &bmp.nibblesStages[(readY * STAGES_BMP_WIDTH) + readX];
    203 
    204 	for (int32_t y = 0; y < 23; y++)
    205 	{
    206 		for (int32_t x = 0; x < 51; x++)
    207 			NI_Screen[x][y] = stagePtr[x];
    208 
    209 		stagePtr += STAGES_BMP_WIDTH;
    210 	}
    211 }
    212 
    213 static void nibblesCreateLevel(int16_t levelNum)
    214 {
    215 	if (levelNum >= NI_MAXLEVEL)
    216 		levelNum = NI_MAXLEVEL-1;
    217 
    218 	nibblesGetLevel(levelNum);
    219 
    220 	int32_t x1 = 0;
    221 	int32_t x2 = 0;
    222 	int32_t y1 = 0;
    223 	int32_t y2 = 0;
    224 
    225 	for (int32_t y = 0; y < 23; y++)
    226 	{
    227 		for (int32_t x = 0; x < 51; x++)
    228 		{
    229 			if (NI_Screen[x][y] == 1 || NI_Screen[x][y] == 3)
    230 			{
    231 				const uint8_t c = NI_Screen[x][y];
    232 
    233 				if (c == 3)
    234 				{
    235 					x1 = x;
    236 					y1 = y;
    237 				}
    238 
    239 				if (c == 1)
    240 				{
    241 					x2 = x;
    242 					y2 = y;
    243 				}
    244 
    245 				NI_Screen[x][y] = 0;
    246 			}
    247 		}
    248 	}
    249 
    250 	const int32_t readX = (51 + 2) * (levelNum % 10);
    251 	const int32_t readY = (23 + 2) * (levelNum / 10);
    252 
    253 	NI_P1Dir = bmp.nibblesStages[(readY * 530) + (readX + 1)];
    254 	NI_P2Dir = bmp.nibblesStages[(readY * 530) + (readX + 0)];
    255 
    256 	NI_P1Len = 5;
    257 	NI_P2Len = 5;
    258 	NI_P1NoClear = 0;
    259 	NI_P2NoClear = 0;
    260 	NI_Number = 0;
    261 	nibblesBuffer[0].length = 0;
    262 	nibblesBuffer[1].length = 0;
    263 
    264 	for (int32_t i = 0; i < 256; i++)
    265 	{
    266 		NI_P1[i].x = (uint8_t)x1;
    267 		NI_P1[i].y = (uint8_t)y1;
    268 		NI_P2[i].x = (uint8_t)x2;
    269 		NI_P2[i].y = (uint8_t)y2;
    270 	}
    271 }
    272 
    273 static void nibbleWriteLevelSprite(int16_t xOut, int16_t yOut, int16_t levelNum)
    274 {
    275 	const int32_t readX = (51 + 2) * (levelNum % 10);
    276 	const int32_t readY = (23 + 2) * (levelNum / 10);
    277 
    278 	const uint8_t *src = (const uint8_t *)&bmp.nibblesStages[(readY * 530) + readX];
    279 	uint32_t *dst = &video.frameBuffer[(yOut * SCREEN_W) + xOut];
    280 
    281 	for (int32_t y = 0; y < 23+2; y++)
    282 	{
    283 		for (int32_t x = 0; x < 51+2; x++)
    284 			dst[x] = video.palette[src[x]];
    285 
    286 		src += 530;
    287 		dst += SCREEN_W;
    288 	}
    289 
    290 	// overwrite start position pixels
    291 	video.frameBuffer[(yOut * SCREEN_W) + (xOut + 0)] = video.palette[PAL_FORGRND];
    292 	video.frameBuffer[(yOut * SCREEN_W) + (xOut + 1)] = video.palette[PAL_FORGRND];
    293 }
    294 
    295 static void highScoreTextOutClipX(uint16_t x, uint16_t y, uint8_t paletteIndex, uint8_t shadowPaletteIndex, const char *textPtr, uint16_t clipX)
    296 {
    297 	assert(textPtr != NULL);
    298 
    299 	uint16_t currX = x;
    300 	for (uint16_t i = 0; i < 22; i++)
    301 	{
    302 		const char ch = textPtr[i];
    303 		if (ch == '\0')
    304 			break;
    305 
    306 		charOutClipX(currX + 1, y + 1, shadowPaletteIndex, ch, clipX); // shadow
    307 		charOutClipX(currX, y, paletteIndex, ch, clipX); // foreground
    308 
    309 		currX += charWidth(ch);
    310 		if (currX >= clipX)
    311 			break;
    312 	}
    313 }
    314 
    315 void nibblesHighScore(void)
    316 {
    317 	if (editor.NI_Play)
    318 	{
    319 		okBox(0, "Nibbles message", "The highscore table is not available during play.", NULL);
    320 		return;
    321 	}
    322 
    323 	clearRect(152, 7, 409, 162);
    324 
    325 	bigTextOut(160, 10, PAL_FORGRND, "Fasttracker Nibbles Highscore");
    326 	for (int16_t i = 0; i < 5; i++)
    327 	{
    328 		highScoreTextOutClipX(160, 42 + (26 * i), PAL_FORGRND, PAL_DSKTOP2, config.NI_HighScore[i].name, 160 + 70);
    329 		hexOutShadow(160 + 76, 42 + (26 * i), PAL_FORGRND, PAL_DSKTOP2, config.NI_HighScore[i].score, 8);
    330 		nibbleWriteLevelSprite(160 + 136, (42 - 9) + (26 * i), config.NI_HighScore[i].level);
    331 
    332 		highScoreTextOutClipX(360, 42 + (26 * i), PAL_FORGRND, PAL_DSKTOP2, config.NI_HighScore[i+5].name, 360 + 70);
    333 		hexOutShadow(360 + 76, 42 + (26 * i), PAL_FORGRND, PAL_DSKTOP2, config.NI_HighScore[i+5].score, 8);
    334 		nibbleWriteLevelSprite(360 + 136, (42 - 9) + (26 * i), config.NI_HighScore[i+5].level);
    335 	}
    336 }
    337 
    338 static void setNibbleDot(uint8_t x, uint8_t y, uint8_t c)
    339 {
    340 	const uint16_t xs = 152 + (x * 8);
    341 	const uint16_t ys = 7 + (y * 7);
    342 
    343 	if (config.NI_Grid)
    344 	{
    345 		fillRect(xs + 0, ys + 0, 8 - 0, 7 - 0, PAL_BUTTON2);
    346 		fillRect(xs + 1, ys + 1, 8 - 1, 7 - 1, c);
    347 	}
    348 	else
    349 	{
    350 		fillRect(xs, ys, 8, 7, c);
    351 	}
    352 
    353 	NI_Screen[x][y] = c;
    354 }
    355 
    356 static void nibblesGenNewNumber(void)
    357 {
    358 	while (true)
    359 	{
    360 		int16_t x = rand() % 51;
    361 		int16_t y = rand() % 23;
    362 
    363 		bool blockIsSuitable;
    364 
    365 		if (y < 22)
    366 			blockIsSuitable = NI_Screen[x][y] == 0 && NI_Screen[x][y+1] == 0;
    367 		else
    368 			blockIsSuitable = NI_Screen[x][y] == 0; // FT2 bugfix: prevent look-up overflow
    369 
    370 		if (blockIsSuitable)
    371 		{
    372 			NI_Number++;
    373 			NI_Screen[x][y] = (uint8_t)(16 + NI_Number);
    374 			NI_NumberX = x;
    375 			NI_NumberY = y;
    376 
    377 			const int16_t xs = 152 + (x * 8);
    378 			const int16_t ys = 7 + (y * 7);
    379 
    380 			if (config.NI_Grid)
    381 			{
    382 				fillRect(xs + 0, ys + 0, 8 - 0, 7 - 0, PAL_BUTTON2);
    383 				fillRect(xs + 1, ys + 1, 8 - 1, 7 - 1, PAL_BCKGRND);
    384 			}
    385 			else
    386 			{
    387 				fillRect(xs, ys, 8, 7, PAL_BCKGRND);
    388 			}
    389 
    390 			drawNibblesFoodNumber((x * 8) + 154, (y * 7) + 7, NI_Number);
    391 
    392 			break;
    393 		}
    394 	}
    395 }
    396 
    397 static void newNibblesGame(void)
    398 {
    399 	nibblesCreateLevel(NI_Level);
    400 	redrawNibblesScreen();
    401 
    402 	setNibbleDot(NI_P1[0].x, NI_P1[0].y, 6);
    403 	if (config.NI_NumPlayers == 1)
    404 		setNibbleDot(NI_P2[0].x, NI_P2[0].y, 7);
    405 
    406 	if (!config.NI_Surround)
    407 		nibblesGenNewNumber();
    408 }
    409 
    410 static bool nibblesInvalid(int16_t x, int16_t y, int16_t d)
    411 {
    412 	if (!config.NI_Wrap)
    413 	{
    414 		if ((x == 0 && d == 0) || (x == 50 && d == 2) || (y == 0 && d == 3) || (y == 22 && d == 1))
    415 			return true;
    416 	}
    417 
    418 	assert(x >= 0 && x < 51 && y >= 0 && y < 23);
    419 	return (NI_Screen[x][y] >= 1 && NI_Screen[x][y] <= 15);
    420 }
    421 
    422 static void drawScoresLives(void)
    423 {
    424 	// player 1
    425 	assert(NI_P1Lives < 100);
    426 	hexOutBg(89, 27, PAL_FORGRND, PAL_DESKTOP, NI_P1Score, 8);
    427 	textOutFixed(131, 39, PAL_FORGRND, PAL_DESKTOP, dec2StrTab[NI_P1Lives]);
    428 
    429 	// player 2
    430 	assert(NI_P2Lives < 100);
    431 	hexOutBg(89, 75, PAL_FORGRND, PAL_DESKTOP, NI_P2Score, 8);
    432 	textOutFixed(131, 87, PAL_FORGRND, PAL_DESKTOP, dec2StrTab[NI_P2Lives]);
    433 }
    434 
    435 static void nibblesDecLives(int16_t l1, int16_t l2)
    436 {
    437 	char name[21+1];
    438 	int16_t i, k;
    439 	highScoreType *h;
    440 
    441 	if (!NI_EternalLives)
    442 	{
    443 		NI_P1Lives -= l1;
    444 		NI_P2Lives -= l2;
    445 	}
    446 
    447 	drawScoresLives();
    448 
    449 	if (l1+l2 == 2)
    450 	{
    451 		okBox(0, "Nibbles message", "Both players died!", NULL);
    452 	}
    453 	else
    454 	{
    455 		if (l2 == 0)
    456 			okBox(0, "Nibbles message", "Player 1 died!", NULL);
    457 		else
    458 			okBox(0, "Nibbles message", "Player 2 died!", NULL);
    459 	}
    460 
    461 	if (NI_P1Lives == 0 || NI_P2Lives == 0)
    462 	{
    463 		editor.NI_Play = false;
    464 		okBox(0, "Nibbles message", "GAME OVER", NULL);
    465 
    466 		// prevent highscore table from showing overflowing level graphics
    467 		if (NI_Level >= NI_MAXLEVEL)
    468 			NI_Level = NI_MAXLEVEL-1;
    469 
    470 		if (NI_P1Score > config.NI_HighScore[9].score)
    471 		{
    472 			strcpy(name, "Unknown");
    473 			inputBox(0, "Player 1 - Enter your name:", name, sizeof (name) - 1);
    474 
    475 			i = 0;
    476 			while (NI_P1Score <= config.NI_HighScore[i].score)
    477 				i++;
    478 
    479 			for (k = 8; k >= i; k--)
    480 				memcpy(&config.NI_HighScore[k+1], &config.NI_HighScore[k], sizeof (highScoreType));
    481 
    482 			if (i == 0)
    483 				okBox(0, "Nibbles message", "You've probably cheated!", NULL);
    484 
    485 			h = &config.NI_HighScore[i];
    486 
    487 			k = (int16_t)strlen(name);
    488 			memset(h->name, 0, sizeof (h->name));
    489 			memcpy(h->name, name, k);
    490 			h->nameLen = (uint8_t)k;
    491 
    492 			h->score = NI_P1Score;
    493 			h->level = NI_Level;
    494 		}
    495 
    496 		if (NI_P2Score > config.NI_HighScore[9].score)
    497 		{
    498 			strcpy(name, "Unknown");
    499 			inputBox(0, "Player 2 - Enter your name:", name, sizeof (name) - 1);
    500 
    501 			i = 0;
    502 			while (NI_P2Score <= config.NI_HighScore[i].score)
    503 				i++;
    504 
    505 			for (k = 8; k >= i; k--)
    506 				memcpy(&config.NI_HighScore[k+1], &config.NI_HighScore[k], sizeof (highScoreType));
    507 
    508 			if (i == 0)
    509 				okBox(0, "Nibbles message", "You've probably cheated!", NULL);
    510 
    511 			h = &config.NI_HighScore[i];
    512 			k = (int16_t)strlen(name);
    513 
    514 			memset(h->name, 0, sizeof (h->name));
    515 			memcpy(h->name, name, k);
    516 			h->nameLen = (uint8_t)k;
    517 
    518 			h->score = NI_P2Score;
    519 			h->level = NI_Level;
    520 		}
    521 
    522 		nibblesHighScore();
    523 	}
    524 	else
    525 	{
    526 		editor.NI_Play = true;
    527 		newNibblesGame();
    528 	}
    529 }
    530 
    531 static void nibblesEraseNumber(void)
    532 {
    533 	if (!config.NI_Surround)
    534 		setNibbleDot((uint8_t)NI_NumberX, (uint8_t)NI_NumberY, 0);
    535 }
    536 
    537 static void nibblesNewLevel(void)
    538 {
    539 	char text[24];
    540 
    541 	sprintf(text, "Level %d finished!", NI_Level+1);
    542 	okBox(0, "Nibbles message", text, NULL);
    543 
    544 	// cast to int16_t to simulate a bug in FT2
    545 	NI_P1Score += 0x10000 + (int16_t)((12 - NI_CurSpeed) * 0x2000);
    546 	if (config.NI_NumPlayers == 1)
    547 		NI_P2Score += 0x10000;
    548 
    549 	NI_Level++;
    550 
    551 	if (NI_P1Lives < 99)
    552 		NI_P1Lives++;
    553 
    554 	if (config.NI_NumPlayers == 1)
    555 	{
    556 		if (NI_P2Lives < 99)
    557 			NI_P2Lives++;
    558 	}
    559 
    560 	NI_Number = 0;
    561 	nibblesCreateLevel(NI_Level);
    562 	redrawNibblesScreen();
    563 
    564 	nibblesGenNewNumber();
    565 }
    566 
    567 void moveNibblesPlayers(void)
    568 {
    569 	if (ui.sysReqShown || --NI_CurTick60Hz != 0)
    570 		return;
    571 
    572 	if (nibblesBufferFull(0))
    573 	{
    574 		switch (nibblesGetBuffer(0))
    575 		{
    576 			case 0: if (NI_P1Dir != 2) NI_P1Dir = 0; break;
    577 			case 1: if (NI_P1Dir != 3) NI_P1Dir = 1; break;
    578 			case 2: if (NI_P1Dir != 0) NI_P1Dir = 2; break;
    579 			case 3: if (NI_P1Dir != 1) NI_P1Dir = 3; break;
    580 			default: break;
    581 		}
    582 	}
    583 
    584 	if (nibblesBufferFull(1))
    585 	{
    586 		switch (nibblesGetBuffer(1))
    587 		{
    588 			case 0: if (NI_P2Dir != 2) NI_P2Dir = 0; break;
    589 			case 1: if (NI_P2Dir != 3) NI_P2Dir = 1; break;
    590 			case 2: if (NI_P2Dir != 0) NI_P2Dir = 2; break;
    591 			case 3: if (NI_P2Dir != 1) NI_P2Dir = 3; break;
    592 			default: break;
    593 		}
    594 	}
    595 
    596 	memmove(&NI_P1[1], &NI_P1[0], 255 * sizeof (nibblesCoord_t));
    597 	if (config.NI_NumPlayers == 1)
    598 		memmove(&NI_P2[1], &NI_P2[0], 255 * sizeof (nibblesCoord_t));
    599 
    600 	switch (NI_P1Dir)
    601 	{
    602 		case 0: NI_P1[0].x++; break;
    603 		case 1: NI_P1[0].y--; break;
    604 		case 2: NI_P1[0].x--; break;
    605 		case 3: NI_P1[0].y++; break;
    606 		default: break;
    607 	}
    608 
    609 	if (config.NI_NumPlayers == 1)
    610 	{
    611 		switch (NI_P2Dir)
    612 		{
    613 			case 0: NI_P2[0].x++; break;
    614 			case 1: NI_P2[0].y--; break;
    615 			case 2: NI_P2[0].x--; break;
    616 			case 3: NI_P2[0].y++; break;
    617 			default: break;
    618 		}
    619 	}
    620 
    621 	if (NI_P1[0].x == 255) NI_P1[0].x = 50;
    622 	if (NI_P2[0].x == 255) NI_P2[0].x = 50;
    623 	if (NI_P1[0].y == 255) NI_P1[0].y = 22;
    624 	if (NI_P2[0].y == 255) NI_P2[0].y = 22;
    625 
    626 	NI_P1[0].x %= 51;
    627 	NI_P1[0].y %= 23;
    628 	NI_P2[0].x %= 51;
    629 	NI_P2[0].y %= 23;
    630 
    631 	if (config.NI_NumPlayers == 1)
    632 	{
    633 		if (nibblesInvalid(NI_P1[0].x, NI_P1[0].y, NI_P1Dir) && nibblesInvalid(NI_P2[0].x, NI_P2[0].y, NI_P2Dir))
    634 		{
    635 			nibblesDecLives(1, 1);
    636 			goto NoMove;
    637 		}
    638 		else if (nibblesInvalid(NI_P1[0].x, NI_P1[0].y, NI_P1Dir))
    639 		{
    640 			nibblesDecLives(1, 0);
    641 			goto NoMove;
    642 		}
    643 		else if (nibblesInvalid(NI_P2[0].x, NI_P2[0].y, NI_P2Dir))
    644 		{
    645 			nibblesDecLives(0, 1);
    646 			goto NoMove;
    647 		}
    648 		else if (NI_P1[0].x == NI_P2[0].x && NI_P1[0].y == NI_P2[0].y)
    649 		{
    650 			nibblesDecLives(1, 1);
    651 			goto NoMove;
    652 		}
    653 	}
    654 	else
    655 	{
    656 		if (nibblesInvalid(NI_P1[0].x, NI_P1[0].y, NI_P1Dir))
    657 		{
    658 			nibblesDecLives(1, 0);
    659 			goto NoMove;
    660 		}
    661 	}
    662 
    663 	int16_t j = 0;
    664 	int16_t i = NI_Screen[NI_P1[0].x][NI_P1[0].y];
    665 	if (i >= 16)
    666 	{
    667 		NI_P1Score += (i & 15) * 999 * (NI_Level + 1);
    668 		nibblesEraseNumber(); j = 1;
    669 		NI_P1NoClear = NI_P1Len >> 1;
    670 	}
    671 
    672 	if (config.NI_NumPlayers == 1)
    673 	{
    674 		i = NI_Screen[NI_P2[0].x][NI_P2[0].y];
    675 		if (i >= 16)
    676 		{
    677 			NI_P2Score += ((i & 15) * 999 * (NI_Level + 1));
    678 			nibblesEraseNumber(); j = 1;
    679 			NI_P2NoClear = NI_P2Len >> 1;
    680 		}
    681 	}
    682 
    683 	NI_P1Score -= 17;
    684 	if (config.NI_NumPlayers == 1)
    685 		NI_P2Score -= 17;
    686 
    687 	if (NI_P1Score < 0) NI_P1Score = 0;
    688 	if (NI_P2Score < 0) NI_P2Score = 0;
    689 
    690 	if (!config.NI_Surround)
    691 	{
    692 		if (NI_P1NoClear > 0 && NI_P1Len < 255)
    693 		{
    694 			NI_P1NoClear--;
    695 			NI_P1Len++;
    696 		}
    697 		else
    698 		{
    699 			setNibbleDot(NI_P1[NI_P1Len].x, NI_P1[NI_P1Len].y, 0);
    700 		}
    701 
    702 		if (config.NI_NumPlayers == 1)
    703 		{
    704 			if (NI_P2NoClear > 0 && NI_P2Len < 255)
    705 			{
    706 				NI_P2NoClear--;
    707 				NI_P2Len++;
    708 			}
    709 			else
    710 			{
    711 				setNibbleDot(NI_P2[NI_P2Len].x, NI_P2[NI_P2Len].y, 0);
    712 			}
    713 		}
    714 	}
    715 
    716 	setNibbleDot(NI_P1[0].x, NI_P1[0].y, 6);
    717 	if (config.NI_NumPlayers == 1)
    718 		setNibbleDot(NI_P2[0].x, NI_P2[0].y, 5);
    719 
    720 	if (j == 1 && !config.NI_Surround)
    721 	{
    722 		if (NI_Number == 9)
    723 		{
    724 			nibblesNewLevel();
    725 			NI_CurTick60Hz = NI_CurSpeed60Hz;
    726 			return;
    727 		}
    728 
    729 		nibblesGenNewNumber();
    730 	}
    731 
    732 NoMove:
    733 	NI_CurTick60Hz = NI_CurSpeed60Hz;
    734 	drawScoresLives();
    735 }
    736 
    737 void showNibblesScreen(void)
    738 {
    739 	if (ui.extendedPatternEditor)
    740 		exitPatternEditorExtended();
    741 
    742 	hideTopScreen();
    743 	ui.nibblesShown = true;
    744 
    745 	drawFramework(0,     0, 632,   3, FRAMEWORK_TYPE1);
    746 	drawFramework(0,     3, 148,  49, FRAMEWORK_TYPE1);
    747 	drawFramework(0,    52, 148,  49, FRAMEWORK_TYPE1);
    748 	drawFramework(0,   101, 148,  72, FRAMEWORK_TYPE1);
    749 	drawFramework(148,   3, 417, 170, FRAMEWORK_TYPE1);
    750 	drawFramework(150,   5, 413, 166, FRAMEWORK_TYPE2);
    751 	drawFramework(565,   3,  67, 170, FRAMEWORK_TYPE1);
    752 
    753 	bigTextOutShadow(4,   6,  PAL_FORGRND, PAL_DSKTOP2, "Player 1");
    754 	bigTextOutShadow(4,  55,  PAL_FORGRND, PAL_DSKTOP2, "Player 2");
    755 
    756 	textOutShadow(4,  27,  PAL_FORGRND, PAL_DSKTOP2, "Score");
    757 	textOutShadow(4,  75,  PAL_FORGRND, PAL_DSKTOP2, "Score");
    758 	textOutShadow(4,  39,  PAL_FORGRND, PAL_DSKTOP2, "Lives");
    759 	textOutShadow(4,  87,  PAL_FORGRND, PAL_DSKTOP2, "Lives");
    760 	textOutShadow(18, 106, PAL_FORGRND, PAL_DSKTOP2, "1 player");
    761 	textOutShadow(18, 120, PAL_FORGRND, PAL_DSKTOP2, "2 players");
    762 	textOutShadow(20, 135, PAL_FORGRND, PAL_DSKTOP2, "Surround");
    763 	textOutShadow(20, 148, PAL_FORGRND, PAL_DSKTOP2, "Grid");
    764 	textOutShadow(20, 161, PAL_FORGRND, PAL_DSKTOP2, "Wrap");
    765 	textOutShadow(80, 105, PAL_FORGRND, PAL_DSKTOP2, "Difficulty:");
    766 	textOutShadow(93, 118, PAL_FORGRND, PAL_DSKTOP2, "Novice");
    767 	textOutShadow(93, 132, PAL_FORGRND, PAL_DSKTOP2, "Average");
    768 	textOutShadow(93, 146, PAL_FORGRND, PAL_DSKTOP2, "Pro");
    769 	textOutShadow(93, 160, PAL_FORGRND, PAL_DSKTOP2, "Triton");
    770 
    771 	drawScoresLives();
    772 
    773 	blitFast(569, 7, bmp.nibblesLogo, 59, 91);
    774 
    775 	showPushButton(PB_NIBBLES_PLAY);
    776 	showPushButton(PB_NIBBLES_HELP);
    777 	showPushButton(PB_NIBBLES_HIGHS);
    778 	showPushButton(PB_NIBBLES_EXIT);
    779 
    780 	checkBoxes[CB_NIBBLES_SURROUND].checked = config.NI_Surround ? true : false;
    781 	checkBoxes[CB_NIBBLES_GRID].checked = config.NI_Grid ? true : false;
    782 	checkBoxes[CB_NIBBLES_WRAP].checked = config.NI_Wrap ? true : false;
    783 	showCheckBox(CB_NIBBLES_SURROUND);
    784 	showCheckBox(CB_NIBBLES_GRID);
    785 	showCheckBox(CB_NIBBLES_WRAP);
    786 
    787 	uncheckRadioButtonGroup(RB_GROUP_NIBBLES_PLAYERS);
    788 	if (config.NI_NumPlayers == 0)
    789 		radioButtons[RB_NIBBLES_1PLAYER].state = RADIOBUTTON_CHECKED;
    790 	else
    791 		radioButtons[RB_NIBBLES_2PLAYERS].state = RADIOBUTTON_CHECKED;
    792 	showRadioButtonGroup(RB_GROUP_NIBBLES_PLAYERS);
    793 
    794 	uncheckRadioButtonGroup(RB_GROUP_NIBBLES_DIFFICULTY);
    795 	switch (config.NI_Speed)
    796 	{
    797 		default:
    798 		case 0: radioButtons[RB_NIBBLES_NOVICE].state  = RADIOBUTTON_CHECKED; break;
    799 		case 1: radioButtons[RB_NIBBLES_AVERAGE].state = RADIOBUTTON_CHECKED; break;
    800 		case 2: radioButtons[RB_NIBBLES_PRO].state     = RADIOBUTTON_CHECKED; break;
    801 		case 3: radioButtons[RB_NIBBLES_TRITON].state  = RADIOBUTTON_CHECKED; break;
    802 	}
    803 	showRadioButtonGroup(RB_GROUP_NIBBLES_DIFFICULTY);
    804 }
    805 
    806 void hideNibblesScreen(void)
    807 {
    808 	hidePushButton(PB_NIBBLES_PLAY);
    809 	hidePushButton(PB_NIBBLES_HELP);
    810 	hidePushButton(PB_NIBBLES_HIGHS);
    811 	hidePushButton(PB_NIBBLES_EXIT);
    812 
    813 	hideRadioButtonGroup(RB_GROUP_NIBBLES_PLAYERS);
    814 	hideRadioButtonGroup(RB_GROUP_NIBBLES_DIFFICULTY);
    815 
    816 	hideCheckBox(CB_NIBBLES_SURROUND);
    817 	hideCheckBox(CB_NIBBLES_GRID);
    818 	hideCheckBox(CB_NIBBLES_WRAP);
    819 
    820 	ui.nibblesShown = false;
    821 }
    822 
    823 void exitNibblesScreen(void)
    824 {
    825 	hideNibblesScreen();
    826 	showTopScreen(true);
    827 }
    828 
    829 // PUSH BUTTONS
    830 
    831 void nibblesPlay(void)
    832 {
    833 	if (editor.NI_Play)
    834 	{
    835 		if (okBox(2, "Nibbles request", "Restart current game of Nibbles?", NULL) != 1)
    836 			return;
    837 	}
    838 
    839 	if (config.NI_Surround && config.NI_NumPlayers == 0)
    840 	{
    841 		okBox(0, "Nibbles message", "Surround mode is not appropriate in one-player mode.", NULL);
    842 		return;
    843 	}
    844 
    845 	if (wallColorsAreCloseToBlack())
    846 		okBox(0, "Nibbles warning", "The Desktop/Button colors are set to values that make the walls hard to see!", NULL);
    847 
    848 	assert(config.NI_Speed < 4);
    849 	NI_CurSpeed = NI_Speeds[config.NI_Speed];
    850 
    851 	// adjust for 70Hz -> 60Hz countdown frames (this is not exact, but we don't want fractional numbers aka. frame skipping)
    852 	NI_CurSpeed60Hz = (uint8_t)SCALE_VBLANK_DELTA_REV(NI_CurSpeed);
    853 	NI_CurTick60Hz = (uint8_t)SCALE_VBLANK_DELTA_REV(NI_Speeds[2]);
    854 
    855 	editor.NI_Play = true;
    856 	NI_P1Score = 0;
    857 	NI_P2Score = 0;
    858 	NI_P1Lives = 5;
    859 	NI_P2Lives = 5;
    860 	NI_Level = 0;
    861 
    862 	newNibblesGame();
    863 }
    864 
    865 void nibblesHelp(void)
    866 {
    867 	if (editor.NI_Play)
    868 	{
    869 		okBox(0, "System message", "Help is not available during play.", NULL);
    870 		return;
    871 	}
    872 
    873 	clearRect(152, 7, 409, 162);
    874 
    875 	bigTextOut(160, 10, PAL_FORGRND, "Fasttracker Nibbles Help");
    876 	for (uint16_t i = 0; i < NIBBLES_HELP_LINES; i++)
    877 		textOut(160, 36 + (11 * i), PAL_BUTTONS, NI_HelpText[i]);
    878 }
    879 
    880 void nibblesExit(void)
    881 {
    882 	if (editor.NI_Play)
    883 	{
    884 		if (okBox(2, "System request", "Quit current game of Nibbles?", NULL) == 1)
    885 		{
    886 			editor.NI_Play = false;
    887 			exitNibblesScreen();
    888 		}
    889 
    890 		return;
    891 	}
    892 
    893 	exitNibblesScreen();
    894 }
    895 
    896 // RADIO BUTTONS
    897 
    898 void nibblesSet1Player(void)
    899 {
    900 	config.NI_NumPlayers = 0;
    901 	checkRadioButton(RB_NIBBLES_1PLAYER);
    902 }
    903 
    904 void nibblesSet2Players(void)
    905 {
    906 	config.NI_NumPlayers = 1;
    907 	checkRadioButton(RB_NIBBLES_2PLAYERS);
    908 }
    909 
    910 void nibblesSetNovice(void)
    911 {
    912 	config.NI_Speed = 0;
    913 	checkRadioButton(RB_NIBBLES_NOVICE);
    914 }
    915 
    916 void nibblesSetAverage(void)
    917 {
    918 	config.NI_Speed = 1;
    919 	checkRadioButton(RB_NIBBLES_AVERAGE);
    920 }
    921 
    922 void nibblesSetPro(void)
    923 {
    924 	config.NI_Speed = 2;
    925 	checkRadioButton(RB_NIBBLES_PRO);
    926 }
    927 
    928 void nibblesSetTriton(void)
    929 {
    930 	config.NI_Speed = 3;
    931 	checkRadioButton(RB_NIBBLES_TRITON);
    932 }
    933 
    934 // CHECK BOXES
    935 
    936 void nibblesToggleSurround(void)
    937 {
    938 	config.NI_Surround ^= 1;
    939 	checkBoxes[CB_NIBBLES_SURROUND].checked = config.NI_Surround ? true : false;
    940 	showCheckBox(CB_NIBBLES_SURROUND);
    941 }
    942 
    943 void nibblesToggleGrid(void)
    944 {
    945 	config.NI_Grid ^= 1;
    946 	checkBoxes[CB_NIBBLES_GRID].checked = config.NI_Grid ? true : false;
    947 	showCheckBox(CB_NIBBLES_GRID);
    948 
    949 	if (editor.NI_Play)
    950 		redrawNibblesScreen();
    951 }
    952 
    953 void nibblesToggleWrap(void)
    954 {
    955 	config.NI_Wrap ^= 1;
    956 
    957 	checkBoxes[CB_NIBBLES_WRAP].checked = config.NI_Wrap ? true : false;
    958 	showCheckBox(CB_NIBBLES_WRAP);
    959 }
    960 
    961 // GLOBAL FUNCTIONS
    962 
    963 void nibblesKeyAdministrator(SDL_Scancode scancode)
    964 {
    965 	if (scancode == SDL_SCANCODE_ESCAPE)
    966 	{
    967 		if (okBox(2, "System request", "Quit current game of Nibbles?", NULL) == 1)
    968 		{
    969 			editor.NI_Play = false;
    970 			exitNibblesScreen();
    971 		}
    972 
    973 		return;
    974 	}
    975 
    976 	switch (scancode)
    977 	{
    978 		// player 1
    979 		case SDL_SCANCODE_RIGHT: nibblesAddBuffer(0, 0); break;
    980 		case SDL_SCANCODE_UP:    nibblesAddBuffer(0, 1); break;
    981 		case SDL_SCANCODE_LEFT:  nibblesAddBuffer(0, 2); break;
    982 		case SDL_SCANCODE_DOWN:  nibblesAddBuffer(0, 3); break;
    983 
    984 		// player 2
    985 		case SDL_SCANCODE_D: nibblesAddBuffer(1, 0); break;
    986 		case SDL_SCANCODE_W: nibblesAddBuffer(1, 1); break;
    987 		case SDL_SCANCODE_A: nibblesAddBuffer(1, 2); break;
    988 		case SDL_SCANCODE_S: nibblesAddBuffer(1, 3); break;
    989 
    990 		default: break;
    991 	}
    992 }
    993 
    994 bool testNibblesCheatCodes(SDL_Keycode keycode) // not directly ported, but same cheatcodes
    995 {
    996 	const char *codeStringPtr;
    997 	uint8_t codeStringLen;
    998 
    999 	// nibbles cheat codes can only be typed in while holding down left SHIFT+CTRL+ALT
   1000 	if (keyb.leftShiftPressed && keyb.leftCtrlPressed && keyb.leftAltPressed)
   1001 	{
   1002 		if (editor.NI_Play)
   1003 		{
   1004 			// during game: "S", "K", "I", "P" (skip to next level)
   1005 			codeStringPtr = nibblesCheatCode1;
   1006 			codeStringLen = sizeof (nibblesCheatCode1) - 1;
   1007 		}
   1008 		else
   1009 		{
   1010 			// not during game: "T", "R", "I", "T", "O", "N" (enable infinite lives)
   1011 			codeStringPtr = nibblesCheatCode2;
   1012 			codeStringLen = sizeof (nibblesCheatCode2) - 1;
   1013 		}
   1014 
   1015 		nibblesCheatBuffer[NI_CheatIndex] = (char)keycode;
   1016 		if (nibblesCheatBuffer[NI_CheatIndex] != codeStringPtr[NI_CheatIndex])
   1017 		{
   1018 			NI_CheatIndex = 0; // start over again, one letter didn't match
   1019 			return true;
   1020 		}
   1021 
   1022 		if (++NI_CheatIndex == codeStringLen) // cheat code was successfully entered
   1023 		{
   1024 			NI_CheatIndex = 0;
   1025 
   1026 			if (editor.NI_Play)
   1027 			{
   1028 				nibblesNewLevel();
   1029 			}
   1030 			else
   1031 			{
   1032 				NI_EternalLives ^= 1;
   1033 				if (NI_EternalLives)
   1034 					okBox(0, "Triton productions declares:", "Eternal lives activated!", NULL);
   1035 				else
   1036 					okBox(0, "Triton productions declares:", "Eternal lives deactivated!", NULL);
   1037 			}
   1038 		}
   1039 
   1040 		return true; // SHIFT+CTRL+ALT held down, don't test other keys
   1041 	}
   1042 
   1043 	return false; // SHIFT+CTRL+ALT not held down, test other keys
   1044 }
   1045 
   1046 void pbNibbles(void)
   1047 {
   1048 	showNibblesScreen();
   1049 }