summary refs log tree commit diff
path: root/sol.c
diff options
context:
space:
mode:
Diffstat (limited to 'sol.c')
-rw-r--r--sol.c392
1 files changed, 392 insertions, 0 deletions
diff --git a/sol.c b/sol.c
new file mode 100644
index 0000000..5622395
--- /dev/null
+++ b/sol.c
@@ -0,0 +1,392 @@
+/* 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 <stdbool.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <time.h>
+
+#include <SDL.h>
+#include <cards.h>
+
+#include "asset.h"
+#include "layout.h"
+#include "stack.h"
+
+#define ARRAY_LEN(a) (sizeof(a) / sizeof((a)[0]))
+
+enum {
+	Stock,
+	Waste1,
+	Waste3,
+	Foundation1,
+	Foundation2,
+	Foundation3,
+	Foundation4,
+	Tableau1,
+	Tableau2,
+	Tableau3,
+	Tableau4,
+	Tableau5,
+	Tableau6,
+	Tableau7,
+	StacksLen,
+};
+
+static struct Stack stacks[StacksLen];
+
+// TODO: Scoring method, score, time.
+static struct {
+	uint draw;
+} game = {
+	.draw = 3,
+};
+
+typedef void StackFn(struct Stack *dst, struct Stack *src, Uint8 len);
+
+static struct {
+	StackFn *fn;
+	uint dst;
+	uint src;
+	uint len;
+} undo;
+
+static void gameDo(StackFn *fn, uint dst, uint src, uint len) {
+	undo.fn = fn;
+	undo.dst = src;
+	undo.src = dst;
+	undo.len = len;
+	fn(&stacks[dst], &stacks[src], len);
+}
+
+static bool gameUndo(void) {
+	if (!undo.fn) return false;
+	undo.fn(&stacks[undo.dst], &stacks[undo.src], undo.len);
+	undo.fn = NULL;
+	return true;
+}
+
+static void gameDeal(void) {
+	undo.fn = NULL;
+	for (uint i = 0; i < StacksLen; ++i) {
+		clear(&stacks[i]);
+	}
+	stacks[Stock] = Deck;
+	shuffle(&stacks[Stock]);
+	for (uint i = Tableau1; i <= Tableau7; ++i) {
+		moveTo(&stacks[i], &stacks[Stock], i - Tableau1);
+		flipTo(&stacks[i], &stacks[Stock], 1);
+	}
+}
+
+static void gameDraw(void) {
+	moveTo(&stacks[Waste1], &stacks[Waste3], stacks[Waste3].len);
+	if (stacks[Stock].len) {
+		if (game.draw > 1) {
+			gameDo(flipTo, Waste3, Stock, game.draw);
+		} else {
+			gameDo(flipTo, Waste1, Stock, 1);
+		}
+	} else {
+		gameDo(flipTo, Stock, Waste1, stacks[Waste1].len);
+	}
+}
+
+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 (card < 0) return false;
+	if (!gameFind(&stack, &index, card)) return false;
+
+	bool top = (card == peek(&stacks[stack]));
+	if (stack == Waste1) {
+		return top && !stacks[Waste3].len;
+	} else if (stack == Waste3) {
+		return top;
+	} else if (stack >= Foundation1 && stack <= Foundation4) {
+		return top;
+	} else if (stack >= Tableau1 && stack <= Tableau7) {
+		return true;
+	} else {
+		return false;
+	}
+}
+
+static bool gameReveal(Card card) {
+	uint stack, index;
+	if (!gameFind(&stack, &index, card)) return false;
+	if (stack < Tableau1 || stack > Tableau7) return false;
+	if (card != peek(&stacks[stack])) return false;
+	if (card > 0) return false;
+	gameDo(flipTo, stack, stack, 1);
+	return true;
+}
+
+static bool gameMove(uint dest, Card card) {
+	uint source, index;
+	if (!gameFind(&source, &index, card)) return false;
+	if (source == dest) return false;
+
+	uint count = stacks[source].len - index;
+	Card destTop = peek(&stacks[dest]);
+
+	if (dest >= Foundation1 && dest <= Foundation4) {
+		if (count > 1) return false;
+		if (!destTop && rank(card) != Cards_A) return false;
+		if (destTop && suit(card) != suit(destTop)) return false;
+		if (destTop && rank(card) != rank(destTop) + 1) return false;
+		gameDo(moveTo, dest, source, 1);
+		return true;
+	}
+
+	if (dest >= Tableau1 && dest <= Tableau7) {
+		if (!destTop && rank(card) != Cards_K) return false;
+		if (destTop && color(card) == color(destTop)) return false;
+		if (destTop && rank(card) != rank(destTop) - 1) return false;
+		gameDo(moveTo, dest, source, count);
+		return true;
+	}
+
+	return false;
+}
+
+enum {
+	CardWidth = Cards_CardWidth,
+	CardHeight = Cards_CardHeight,
+
+	StackMarginX = 11,
+	StackMarginY = 6,
+
+	StockX = StackMarginX,
+	StockY = StackMarginY,
+
+	WasteX = StockX + CardWidth + StackMarginX,
+	WasteY = StackMarginY,
+
+	FoundationX = WasteX + 2 * (CardWidth + StackMarginX),
+	FoundationY = StackMarginY,
+
+	TableauX = StackMarginX,
+	TableauY = StockY + CardHeight + StackMarginY,
+
+	FanRightDeltaX = 14,
+	FanDownDeltaYBack = 3,
+	FanDownDeltaYFront = 15,
+
+	WindowWidth = 7 * CardWidth + 8 * StackMarginX,
+	WindowHeight = 2 * (StackMarginY + CardHeight)
+		+ 6 * FanDownDeltaYBack
+		+ 12 * FanDownDeltaYFront
+		+ StackMarginY,
+};
+
+static const struct Style FanRight = {
+	1, 0, 0, FanRightDeltaX, SquareDeltaY
+};
+static const struct Style FanDown = {
+	1, 0, FanDownDeltaYBack, 0, FanDownDeltaYFront
+};
+
+static struct SDL_Rect stackRects[StacksLen];
+static struct Layout layout;
+static uint backTexture = Cards_Back1;
+
+static void updateLayout(void) {
+	layoutClear(&layout);
+
+	SDL_Rect stock = { StockX, StockY, CardWidth, CardHeight };
+	stackRects[Stock] = stock;
+	listPush(&layout.main, &stock, Cards_O);
+	layoutStack(&layout, &stock, &stacks[Stock], &Square);
+
+	SDL_Rect waste = { WasteX, WasteY, CardWidth, CardHeight };
+	layoutStack(&layout, &waste, &stacks[Waste1], &Square);
+	layoutStack(&layout, &waste, &stacks[Waste3], &FanRight);
+
+	SDL_Rect found = { FoundationX, FoundationY, CardWidth, CardHeight };
+	for (uint i = Foundation1; i <= Foundation4; ++i) {
+		stackRects[i] = found;
+		listPush(&layout.main, &found, Cards_Empty);
+		SDL_Rect rect = found;
+		layoutStack(&layout, &rect, &stacks[i], &Square);
+		found.x += CardWidth + StackMarginX;
+	}
+
+	SDL_Rect table = { TableauX, TableauY, CardWidth, CardHeight };
+	for (uint i = Tableau1; i <= Tableau7; ++i) {
+		stackRects[i] = table;
+		stackRects[i].h = WindowHeight;
+		SDL_Rect rect = table;
+		layoutStack(&layout, &rect, &stacks[i], &FanDown);
+		table.x += CardWidth + StackMarginX;
+	}
+}
+
+static bool keyDown(SDL_KeyboardEvent key) {
+	switch (key.keysym.sym) {
+		case SDLK_F2: gameDeal(); return true;
+		case SDLK_BACKSPACE: return gameUndo();
+		case SDLK_b: {
+			backTexture -= Cards_Back1;
+			backTexture += 1;
+			backTexture %= 12;
+			backTexture += Cards_Back1;
+			return true;
+		}
+		case SDLK_d: {
+			game.draw = (game.draw == 1 ? 3 : 1);
+			gameDeal();
+			return true;
+		}
+		default: return false;
+	}
+}
+
+static bool mouseButtonDown(SDL_MouseButtonEvent button) {
+	struct SDL_Point point = { button.x, button.y };
+	if (SDL_PointInRect(&point, &stackRects[Stock])) {
+		gameDraw();
+		return true;
+	}
+	struct Item *item = listFind(&layout.main, &point);
+	if (!item) return false;
+	if (!gameAvail(item->card)) return gameReveal(item->card);
+	if (button.clicks % 2 == 0) {
+		for (uint dest = Foundation1; dest <= Foundation4; ++dest) {
+			if (gameMove(dest, item->card)) return true;
+		}
+		return false;
+	}
+	layout.dragItem = *item;
+	return true;
+}
+
+static bool mouseButtonUp(SDL_MouseButtonEvent button) {
+	(void)button;
+	if (!layout.dragItem.card) return false;
+	for (uint dest = 0; dest < StacksLen; ++dest) {
+		if (SDL_HasIntersection(&layout.dragItem.rect, &stackRects[dest])) {
+			if (gameMove(dest, layout.dragItem.card)) break;
+		}
+	}
+	layout.dragItem.card = 0;
+	return true;
+}
+
+static bool mouseMotion(SDL_MouseMotionEvent motion) {
+	if (!motion.state) return false;
+	layout.dragItem.rect.x += motion.xrel;
+	layout.dragItem.rect.y += motion.yrel;
+	return true;
+}
+
+static SDL_Renderer *render;
+static SDL_Texture *textures[Cards_CardCount];
+
+static void renderList(const struct List *list) {
+	for (uint i = 0; i < list->len; ++i) {
+		int tex = list->items[i].card;
+		if (tex < 0) tex = backTexture;
+		SDL_RenderCopy(render, textures[tex], NULL, &list->items[i].rect);
+	}
+}
+
+static void err(const char *title) {
+	int error = SDL_ShowSimpleMessageBox(
+		SDL_MESSAGEBOX_ERROR, title, SDL_GetError(), NULL
+	);
+	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);
+
+	struct Paths paths;
+	if (assetPaths(&paths) < 0) err("SDL_GetPrefPath");
+
+	SDL_RWops *rw = assetOpenCards(&paths);
+	if (!rw) return EXIT_FAILURE;
+
+	SDL_Surface *surfaces[Cards_CardCount];
+	int error = Cards_LoadCards(
+		surfaces, Cards_CardCount,
+		rw, Cards_ColorKey | Cards_AlphaCorners | Cards_BlackBorders
+	);
+	if (error) err("Cards_LoadCards");
+	SDL_RWclose(rw);
+
+	SDL_Window *window;
+	error = SDL_CreateWindowAndRenderer(
+		WindowWidth, WindowHeight, SDL_WINDOW_ALLOW_HIGHDPI,
+		&window, &render
+	);
+	if (error) err("SDL_CreateWindowAndRenderer");
+	SDL_SetWindowTitle(window, "Solitaire");
+
+	SDL_RenderSetIntegerScale(render, SDL_TRUE);
+	SDL_RenderSetLogicalSize(render, WindowWidth, WindowHeight);
+
+	for (uint i = 0; i < Cards_CardCount; ++i) {
+		textures[i] = NULL;
+		if (!surfaces[i]) continue;
+		textures[i] = SDL_CreateTextureFromSurface(render, surfaces[i]);
+		if (!textures[i]) err("SDL_CreateTextureFromSurface");
+		SDL_FreeSurface(surfaces[i]);
+	}
+
+	srand(time(NULL));
+	backTexture = Cards_Back1 + randUniform(12);
+	gameDeal();
+
+	for (;;) {
+		updateLayout();
+
+		SDL_SetRenderDrawColor(render, 0x00, 0xAA, 0x55, 0xFF);
+		SDL_RenderClear(render);
+		renderList(&layout.main);
+		renderList(&layout.drag);
+		SDL_RenderPresent(render);
+
+		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;
+			} else if (event.type == SDL_MOUSEBUTTONUP) {
+				if (mouseButtonUp(event.button)) break;
+			} else if (event.type == SDL_MOUSEMOTION) {
+				if (mouseMotion(event.motion)) break;
+			}
+		}
+	}
+
+quit:
+	SDL_Quit();
+}