summary refs log tree commit diff
path: root/freecell.c
diff options
context:
space:
mode:
Diffstat (limited to 'freecell.c')
-rw-r--r--freecell.c357
1 files changed, 357 insertions, 0 deletions
diff --git a/freecell.c b/freecell.c
new file mode 100644
index 0000000..487ef79
--- /dev/null
+++ b/freecell.c
@@ -0,0 +1,357 @@
+/* Copyright (C) 2019  C. McEnroe <june@causal.agency>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <SDL.h>
+#include <stdbool.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <time.h>
+
+#include "cards.h"
+#include "layout.h"
+#include "stack.h"
+
+#define ARRAY_LEN(a) (sizeof(a) / sizeof((a)[0]))
+
+enum {
+	Foundation1,
+	Foundation2,
+	Foundation3,
+	Foundation4,
+	Cell1,
+	Cell2,
+	Cell3,
+	Cell4,
+	Tableau1,
+	Tableau2,
+	Tableau3,
+	Tableau4,
+	Tableau5,
+	Tableau6,
+	Tableau7,
+	Tableau8,
+	StacksLen,
+};
+
+static struct Stack stacks[StacksLen];
+
+static void gameDeal(void) {
+	for (uint i = 0; i < StacksLen; ++i) {
+		stackClear(&stacks[i]);
+	}
+	struct Stack deck = {0};
+	for (Card i = 1; i <= 52; ++i) {
+		stackPush(&deck, i);
+	}
+	stackShuffle(&deck);
+	for (uint i = Tableau1; i <= Tableau4; ++i) {
+		stackMoveTo(&stacks[i], &deck, 7);
+	}
+	for (uint i = Tableau5; i <= Tableau8; ++i) {
+		stackMoveTo(&stacks[i], &deck, 6);
+	}
+}
+
+static bool gameFind(uint *stack, uint *index, Card card) {
+	for (*stack = 0; *stack < StacksLen; ++*stack) {
+		for (*index = 0; *index < stacks[*stack].len; ++*index) {
+			if (stacks[*stack].cards[*index] == card) return true;
+		}
+	}
+	return false;
+}
+
+static bool gameAvail(Card card) {
+	uint stack, index;
+	if (!gameFind(&stack, &index, card)) return false;
+	if (stack >= Foundation1 && stack <= Foundation4) return false;
+	return card == stackTop(&stacks[stack]);
+}
+
+static bool gameMove(uint dest, Card card) {
+	uint source, index;
+	if (!gameFind(&source, &index, card)) return false;
+	Card destTop = stackTop(&stacks[dest]);
+
+	if (source == dest) return false;
+	if (dest >= Cell1 && dest <= Cell4) {
+		if (stacks[dest].len) return false;
+	}
+	if (dest >= Foundation1 && dest <= Foundation4) {
+		if (!destTop && cardRank(card) != Cards_A) return false;
+		if (destTop && cardSuit(card) != cardSuit(destTop)) return false;
+		if (destTop && cardRank(card) != cardRank(destTop) + 1) return false;
+	}
+	if (dest >= Tableau1 && dest <= Tableau8) {
+		if (destTop && cardColor(card) == cardColor(destTop)) return false;
+		if (destTop && cardRank(card) != cardRank(destTop) - 1) return false;
+	}
+
+	stackPush(&stacks[dest], stackPop(&stacks[source]));
+	return true;
+}
+
+static bool gameAuto(void) {
+	Card min[2] = { Cards_K, Cards_K };
+	for (uint i = Cell1; i <= Tableau8; ++i) {
+		for (uint j = 0; j < stacks[i].len; ++j) {
+			Card card = stacks[i].cards[j];
+			if (cardRank(card) < min[cardColor(card)]) {
+				min[cardColor(card)] = cardRank(card);
+			}
+		}
+	}
+	for (uint i = Cell1; i <= Tableau8; ++i) {
+		Card card = stackTop(&stacks[i]);
+		if (!card) continue;
+		if (cardRank(card) > Cards_2) {
+			if (min[!cardColor(card)] < cardRank(card)) continue;
+		}
+		for (uint dest = Foundation1; dest <= Foundation4; ++dest) {
+			if (gameMove(dest, card)) return true;
+		}
+	}
+	return false;
+}
+
+enum {
+	StackMarginX = 7,
+	StackMarginY = 10,
+
+	CellX = 0,
+	CellY = 0,
+
+	CellsMarginX = 64,
+
+	FoundationX = CellX + 4 * Cards_Width + CellsMarginX,
+	FoundationY = CellY,
+
+	TableauX = StackMarginX,
+	TableauY = CellY + Cards_Height + StackMarginY,
+
+	FanDownDeltaY = 17,
+
+	WindowWidth = 8 * Cards_Width + 9 * StackMarginX + 1,
+	WindowHeight = TableauY + Cards_Height
+		+ 13 * FanDownDeltaY
+		+ StackMarginY,
+};
+
+static const struct Style FanDown = { 1, 0, 0, 0, FanDownDeltaY };
+
+static struct SDL_Rect stackRects[StacksLen];
+static struct Layout layout;
+
+static void updateLayout(void) {
+	layoutClear(&layout);
+
+	SDL_Rect cell = { CellX, CellY, Cards_Width, Cards_Height };
+	for (uint i = Cell1; i <= Cell4; ++i) {
+		stackRects[i] = cell;
+		layoutStack(&layout, &cell, &stacks[i], &Flat);
+		cell.x += Cards_Width;
+	}
+
+	SDL_Rect found = { FoundationX, FoundationY, Cards_Width, Cards_Height };
+	for (uint i = Foundation1; i <= Foundation4; ++i) {
+		stackRects[i] = found;
+		layoutStack(&layout, &found, &stacks[i], &Flat);
+		found.x += Cards_Width;
+	}
+
+	SDL_Rect table = { TableauX, TableauY, Cards_Width, Cards_Height };
+	for (uint i = Tableau1; i <= Tableau8; ++i) {
+		stackRects[i] = table;
+		stackRects[i].h = WindowHeight;
+		SDL_Rect rect = table;
+		layoutStack(&layout, &rect, &stacks[i], &FanDown);
+		table.x += Cards_Width + StackMarginX;
+	}
+}
+
+static bool keyDown(SDL_KeyboardEvent key) {
+	switch (key.keysym.sym) {
+		case SDLK_F2: gameDeal(); return true;
+		default: return false;
+	}
+}
+
+static bool mouseButtonDown(SDL_MouseButtonEvent button) {
+	struct SDL_Point point = { button.x, button.y };
+
+	if (layout.dragItem.card) {
+		if (button.clicks % 2 == 0) {
+			for (uint dest = Cell1; dest <= Cell4; ++dest) {
+				if (gameMove(dest, layout.dragItem.card)) {
+					layout.dragItem.card = 0;
+					return true;
+				}
+			}
+		}
+		for (uint dest = 0; dest < StacksLen; ++dest) {
+			if (SDL_PointInRect(&point, &stackRects[dest])) {
+				if (gameMove(dest, layout.dragItem.card)) break;
+			}
+		}
+		layout.dragItem.card = 0;
+		return true;
+	}
+
+	// TODO: Right click to reveal?
+	struct Item *item = listFind(&layout.main, &point);
+	if (!item) return false;
+	if (!gameAvail(item->card)) return false;
+	layout.dragItem = *item;
+	return true;
+}
+
+static SDL_Window *window;
+static SDL_Renderer *render;
+
+static void renderOutlines(void) {
+	for (uint i = Foundation1; i <= Cell4; ++i) {
+		int right = stackRects[i].x + Cards_Width - 1;
+		int bottom = stackRects[i].y + Cards_Height - 1;
+		SDL_Point black[3] = {
+			{ stackRects[i].x, bottom - 1 },
+			{ stackRects[i].x, stackRects[i].y },
+			{ right - 1, stackRects[i].y },
+		};
+		SDL_Point green[3] = {
+			{ stackRects[i].x + 1, bottom },
+			{ right, bottom },
+			{ right, stackRects[i].y + 1 },
+		};
+		SDL_SetRenderDrawColor(render, 0x00, 0x00, 0x00, 0xFF);
+		SDL_RenderDrawLines(render, black, 3);
+		SDL_SetRenderDrawColor(render, 0x00, 0xFF, 0x00, 0xFF);
+		SDL_RenderDrawLines(render, green, 3);
+	}
+}
+
+static void renderList(SDL_Texture **textures, const struct List *list) {
+	for (uint i = 0; i < list->len; ++i) {
+		SDL_RenderCopy(
+			render, textures[list->items[i].card],
+			NULL, &list->items[i].rect
+		);
+	}
+}
+
+static void err(const char *title) {
+	int error = SDL_ShowSimpleMessageBox(
+		SDL_MESSAGEBOX_ERROR, title, SDL_GetError(), window
+	);
+	if (error) fprintf(stderr, "%s\n", SDL_GetError());
+	exit(EXIT_FAILURE);
+}
+
+int main(void) {
+	if (SDL_Init(SDL_INIT_VIDEO) < 0) err("SDL_Init");
+	atexit(SDL_Quit);
+
+	char *prefPath = SDL_GetPrefPath("Causal Agency", "Cards");
+	if (!prefPath) err("SDL_GetPrefPath");
+
+	char *basePath = SDL_GetBasePath();
+	if (!basePath) err("SDL_GetBasePath");
+
+	const char *paths[] = { prefPath, basePath, "" };
+	const char *names[] = { "CARDS.DLL", "SOL.EXE", "cards.dll" };
+
+	SDL_RWops *rw = NULL;
+	for (uint i = 0; !rw && i < ARRAY_LEN(paths); ++i) {
+		for (uint j = 0; !rw && j < ARRAY_LEN(names); ++j) {
+			char path[1024];
+			snprintf(path, sizeof(path), "%s%s", paths[i], names[j]);
+			rw = SDL_RWFromFile(path, "rb");
+		}
+	}
+	if (!rw) {
+		char msg[4096];
+		snprintf(
+			msg, sizeof(msg), "CARDS.DLL or SOL.EXE not found in:\n%s\n%s",
+			prefPath, basePath
+		);
+		SDL_ShowSimpleMessageBox(SDL_MESSAGEBOX_ERROR, "Cards", msg, NULL);
+		return EXIT_FAILURE;
+	}
+
+	struct Cards *cards = Cards_Load(
+		rw, Cards_ColorKey | Cards_AlphaCorners | Cards_BlackBorders
+	);
+	if (!cards) err("Cards_Load");
+	SDL_RWclose(rw);
+
+	int error = SDL_CreateWindowAndRenderer(
+		WindowWidth, WindowHeight, SDL_WINDOW_ALLOW_HIGHDPI,
+		&window, &render
+	);
+	if (error) err("SDL_CreateWindowAndRenderer");
+	SDL_SetWindowTitle(window, "FreeCell");
+
+	SDL_RenderSetIntegerScale(render, SDL_TRUE);
+	SDL_RenderSetLogicalSize(render, WindowWidth, WindowHeight);
+
+	SDL_Texture *textures[Cards_Count];
+	SDL_Texture *inverted[Cards_Count];
+	for (uint i = 0; i < Cards_Count; ++i) {
+		textures[i] = NULL;
+		if (!cards->surfaces[i]) continue;
+		textures[i] = SDL_CreateTextureFromSurface(render, cards->surfaces[i]);
+		if (!textures[i]) err("SDL_CreateTextureFromSurface");
+	}
+	if (Cards_Invert(cards) < 0) err("Cards_Invert");
+	for (uint i = 0; i < Cards_Count; ++i) {
+		inverted[i] = NULL;
+		if (!cards->surfaces[i]) continue;
+		inverted[i] = SDL_CreateTextureFromSurface(render, cards->surfaces[i]);
+		if (!inverted[i]) err("SDL_CreateTextureFromSurface");
+	}
+	Cards_Free(cards);
+
+	srand(time(NULL));
+	gameDeal();
+
+	for (;;) {
+		updateLayout();
+
+		SDL_SetRenderDrawColor(render, 0x00, 0xAA, 0x55, 0xFF);
+		SDL_RenderClear(render);
+		renderOutlines();
+		renderList(textures, &layout.main);
+		renderList(inverted, &layout.drag);
+		SDL_RenderPresent(render);
+
+		// TODO: Add more than a frame delay between automatic moves?
+		if (gameAuto()) continue;
+
+		SDL_Event event;
+		for (;;) {
+			SDL_WaitEvent(&event);
+			if (event.type == SDL_QUIT) {
+				goto quit;
+			} else if (event.type == SDL_KEYDOWN) {
+				if (keyDown(event.key)) break;
+			} else if (event.type == SDL_MOUSEBUTTONDOWN) {
+				if (mouseButtonDown(event.button)) break;
+			}
+		}
+	}
+
+quit:
+	SDL_Quit();
+}