diff options
Diffstat (limited to '')
-rw-r--r-- | freecell.c | 357 |
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(); +} |