/* Copyright (C) 2019 C. McEnroe * * 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 . */ #include #include #include #include #include #include "cards.h" #include "stack.h" typedef unsigned uint; 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) { stackClear(&stacks[i]); } stackDeck(&stacks[Stock]); stackShuffle(&stacks[Stock]); for (uint i = Tableau1; i <= Tableau7; ++i) { stackMoveTo(&stacks[i], &stacks[Stock], i - Tableau1); stackFlipTo(&stacks[i], &stacks[Stock], 1); } } static void gameDraw(void) { stackMoveTo(&stacks[Waste1], &stacks[Waste3], stacks[Waste3].len); if (stacks[Stock].len) { if (game.draw > 1) { gameDo(stackFlipTo, Waste3, Stock, game.draw); } else { gameDo(stackFlipTo, Waste1, Stock, 1); } } else { gameDo(stackFlipTo, 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 = (index == stacks[stack].len - 1); 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 (index != stacks[stack].len - 1) return false; if (card > 0) return false; gameDo(stackFlipTo, 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 = stackTop(&stacks[dest]); if (dest >= Foundation1 && dest <= Foundation4) { if (count > 1) return false; 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; gameDo(stackMoveTo, dest, source, 1); return true; } if (dest >= Tableau1 && dest <= Tableau7) { if (!destTop && cardRank(card) != Cards_K) return false; if (destTop && cardColor(card) == cardColor(destTop)) return false; if (destTop && cardRank(card) != cardRank(destTop) - 1) return false; gameDo(stackMoveTo, dest, source, count); return true; } return false; } enum { StackMarginX = 11, StackMarginY = 6, StockX = StackMarginX, StockY = StackMarginY, WasteX = StockX + Cards_Width + StackMarginX, WasteY = StackMarginY, FoundationX = WasteX + 2 * (Cards_Width + StackMarginX), FoundationY = StackMarginY, TableauX = StackMarginX, TableauY = StockY + Cards_Height + StackMarginY, StackDeltaX = 2, StackDeltaY = 1, Waste3DeltaX = 14, Waste3DeltaY = StackDeltaY, TableauDeltaX = 0, TableauDeltaYBack = 3, TableauDeltaYFront = 15, WindowWidth = 7 * Cards_Width + 8 * StackMarginX, WindowHeight = 2 * (StackMarginY + Cards_Height) + 6 * TableauDeltaYBack + 12 * TableauDeltaYFront + StackMarginY, }; struct Item { SDL_Rect rect; uint texture; Card card; }; struct List { uint len; struct Item items[52]; }; static void listPush(struct List *list, struct Item item) { assert(list->len < 52); list->items[list->len++] = item; } static struct { SDL_Rect stacks[StacksLen]; struct List base; struct List main; struct List drag; struct Item dragItem; uint backTexture; } layout; static struct Item *layoutFind(const SDL_Point *point) { for (uint i = layout.main.len - 1; i < layout.main.len; --i) { if (SDL_PointInRect(point, &layout.main.items[i].rect)) { return &layout.main.items[i]; } } return NULL; } static void layoutClear(void) { layout.base.len = 0; layout.main.len = 0; layout.drag.len = 0; } static void layoutCard(struct List **list, SDL_Rect *rect, Card card) { if (card == layout.dragItem.card) { *list = &layout.drag; *rect = layout.dragItem.rect; } struct Item item = { *rect, (card > 0 ? card : layout.backTexture), card }; listPush(*list, item); } static void layoutStack(SDL_Rect *rect, const struct Stack *stack, uint depth) { struct List *list = &layout.main; for (uint i = 0; i < stack->len; ++i) { layoutCard(&list, rect, stack->cards[i]); if (i > 0 && i % (depth / 3) == 0) { rect->x += StackDeltaX; rect->y += StackDeltaY; } } } static void layoutStock(void) { SDL_Rect rect = { StockX, StockY, Cards_Width, Cards_Height }; struct Item item = { rect, Cards_O, 0 }; listPush(&layout.base, item); layout.stacks[Stock] = rect; layoutStack(&rect, &stacks[Stock], 24); } static void layoutWaste(void) { SDL_Rect rect = { WasteX, WasteY, Cards_Width, Cards_Height }; layoutStack(&rect, &stacks[Waste1], 24); struct List *list = &layout.main; for (uint i = 0; i < stacks[Waste3].len; ++i) { layoutCard(&list, &rect, stacks[Waste3].cards[i]); rect.x += Waste3DeltaX; rect.y += Waste3DeltaY; } } static void layoutFoundations(void) { SDL_Rect base = { FoundationX, FoundationY, Cards_Width, Cards_Height }; for (uint i = Foundation1; i <= Foundation4; ++i) { struct Item item = { base, Cards_Empty, 0 }; listPush(&layout.base, item); layout.stacks[i] = base; SDL_Rect rect = base; layoutStack(&rect, &stacks[i], 13); base.x += Cards_Width + StackMarginX; } } static void layoutTableau(void) { SDL_Rect base = { TableauX, TableauY, Cards_Width, Cards_Height }; for (uint i = Tableau1; i <= Tableau7; ++i) { SDL_Rect stack = { base.x, base.y, base.w, WindowHeight }; layout.stacks[i] = stack; struct List *list = &layout.main; SDL_Rect rect = base; for (uint j = 0; j < stacks[i].len; ++j) { Card card = stacks[i].cards[j]; layoutCard(&list, &rect, card); rect.x += TableauDeltaX; rect.y += (card > 0 ? TableauDeltaYFront : TableauDeltaYBack); } base.x += Cards_Width + 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: { layout.backTexture -= Cards_Back1; layout.backTexture += 1; layout.backTexture %= 12; layout.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, &layout.stacks[Stock])) { gameDraw(); return true; } struct Item *item = layoutFind(&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, &layout.stacks[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_Window *window; static SDL_Renderer *render; static SDL_Texture *textures[Cards_Count]; static void renderList(const struct List *list) { for (uint i = 0; i < list->len; ++i) { SDL_RenderCopy( render, textures[list->items[i].texture], 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"); char paths[4][1024]; snprintf(paths[0], sizeof(paths[0]), "%sCARDS.DLL", prefPath); snprintf(paths[1], sizeof(paths[1]), "%sSOL.EXE", prefPath); snprintf(paths[2], sizeof(paths[2]), "%sCARDS.DLL", basePath); snprintf(paths[3], sizeof(paths[3]), "%sSOL.EXE", basePath); SDL_RWops *rw; for (uint i = 0; i < 4; ++i) { rw = SDL_RWFromFile(paths[i], "rb"); if (rw) break; } if (!rw) { char buf[4096]; snprintf( buf, sizeof(buf), "CARDS.DLL not found in:\n%s\n%s", prefPath, basePath ); SDL_ShowSimpleMessageBox(SDL_MESSAGEBOX_ERROR, "Cards", buf, 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, "Solitaire"); int width, height; SDL_GetRendererOutputSize(render, &width, &height); if (width > WindowWidth && height > WindowHeight) { SDL_RenderSetIntegerScale(render, SDL_TRUE); SDL_RenderSetScale( render, (float)width / WindowWidth, (float)height / WindowHeight ); } 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"); } Cards_Free(cards); srand(time(NULL)); layout.backTexture = Cards_Back1 + randUniform(12); gameDeal(); for (;;) { layoutClear(); layoutStock(); layoutWaste(); layoutFoundations(); layoutTableau(); SDL_SetRenderDrawColor(render, 0x00, 0xAA, 0x55, 0xFF); SDL_RenderClear(render); renderList(&layout.base); 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(); }