This commit is contained in:
2026-06-22 15:32:32 -05:00
commit f5ab324cca
27 changed files with 3024 additions and 0 deletions

500
lib/annexc-data.ts Normal file
View File

@@ -0,0 +1,500 @@
// Annex C der FIFA WM 2026 Wettbewerbsregeln: 495 Szenarien.
// Schluessel = 8 Gruppen (alphabetisch) der besten Drittplatzierten.
// Wert = Zuordnung in Spaltenreihenfolge der Gruppensieger [1A,1B,1D,1E,1G,1I,1K,1L].
export const ANNEX_C: Record<string,string> = {
"EFGHIJKL": "EJIFHGLK",
"DFGHIJKL": "HGIDJFLK",
"DEGHIJKL": "EJIDHGLK",
"DEFHIJKL": "EJIDHFLK",
"DEFGIJKL": "EGIDJFLK",
"DEFGHJKL": "EGJDHFLK",
"DEFGHIKL": "EGIDHFLK",
"DEFGHIJL": "EGJDHFLI",
"DEFGHIJK": "EGJDHFIK",
"CFGHIJKL": "HGICJFLK",
"CEGHIJKL": "EJICHGLK",
"CEFHIJKL": "EJICHFLK",
"CEFGIJKL": "EGICJFLK",
"CEFGHJKL": "EGJCHFLK",
"CEFGHIKL": "EGICHFLK",
"CEFGHIJL": "EGJCHFLI",
"CEFGHIJK": "EGJCHFIK",
"CDGHIJKL": "HGICJDLK",
"CDFHIJKL": "CJIDHFLK",
"CDFGIJKL": "CGIDJFLK",
"CDFGHJKL": "CGJDHFLK",
"CDFGHIKL": "CGIDHFLK",
"CDFGHIJL": "CGJDHFLI",
"CDFGHIJK": "CGJDHFIK",
"CDEHIJKL": "EJICHDLK",
"CDEGIJKL": "EGICJDLK",
"CDEGHJKL": "EGJCHDLK",
"CDEGHIKL": "EGICHDLK",
"CDEGHIJL": "EGJCHDLI",
"CDEGHIJK": "EGJCHDIK",
"CDEFIJKL": "CJEDIFLK",
"CDEFHJKL": "CJEDHFLK",
"CDEFHIKL": "CEIDHFLK",
"CDEFHIJL": "CJEDHFLI",
"CDEFHIJK": "CJEDHFIK",
"CDEFGJKL": "CGEDJFLK",
"CDEFGIKL": "CGEDIFLK",
"CDEFGIJL": "CGEDJFLI",
"CDEFGIJK": "CGEDJFIK",
"CDEFGHKL": "CGEDHFLK",
"CDEFGHJL": "CGJDHFLE",
"CDEFGHJK": "CGJDHFEK",
"CDEFGHIL": "CGEDHFLI",
"CDEFGHIK": "CGEDHFIK",
"CDEFGHIJ": "CGJDHFEI",
"BFGHIJKL": "HJBFIGLK",
"BEGHIJKL": "EJIBHGLK",
"BEFHIJKL": "EJBFIHLK",
"BEFGIJKL": "EJBFIGLK",
"BEFGHJKL": "EJBFHGLK",
"BEFGHIKL": "EGBFIHLK",
"BEFGHIJL": "EJBFHGLI",
"BEFGHIJK": "EJBFHGIK",
"BDGHIJKL": "HJBDIGLK",
"BDFHIJKL": "HJBDIFLK",
"BDFGIJKL": "IGBDJFLK",
"BDFGHJKL": "HGBDJFLK",
"BDFGHIKL": "HGBDIFLK",
"BDFGHIJL": "HGBDJFLI",
"BDFGHIJK": "HGBDJFIK",
"BDEHIJKL": "EJBDIHLK",
"BDEGIJKL": "EJBDIGLK",
"BDEGHJKL": "EJBDHGLK",
"BDEGHIKL": "EGBDIHLK",
"BDEGHIJL": "EJBDHGLI",
"BDEGHIJK": "EJBDHGIK",
"BDEFIJKL": "EJBDIFLK",
"BDEFHJKL": "EJBDHFLK",
"BDEFHIKL": "EIBDHFLK",
"BDEFHIJL": "EJBDHFLI",
"BDEFHIJK": "EJBDHFIK",
"BDEFGJKL": "EGBDJFLK",
"BDEFGIKL": "EGBDIFLK",
"BDEFGIJL": "EGBDJFLI",
"BDEFGIJK": "EGBDJFIK",
"BDEFGHKL": "EGBDHFLK",
"BDEFGHJL": "HGBDJFLE",
"BDEFGHJK": "HGBDJFEK",
"BDEFGHIL": "EGBDHFLI",
"BDEFGHIK": "EGBDHFIK",
"BDEFGHIJ": "HGBDJFEI",
"BCGHIJKL": "HJBCIGLK",
"BCFHIJKL": "HJBCIFLK",
"BCFGIJKL": "IGBCJFLK",
"BCFGHJKL": "HGBCJFLK",
"BCFGHIKL": "HGBCIFLK",
"BCFGHIJL": "HGBCJFLI",
"BCFGHIJK": "HGBCJFIK",
"BCEHIJKL": "EJBCIHLK",
"BCEGIJKL": "EJBCIGLK",
"BCEGHJKL": "EJBCHGLK",
"BCEGHIKL": "EGBCIHLK",
"BCEGHIJL": "EJBCHGLI",
"BCEGHIJK": "EJBCHGIK",
"BCEFIJKL": "EJBCIFLK",
"BCEFHJKL": "EJBCHFLK",
"BCEFHIKL": "EIBCHFLK",
"BCEFHIJL": "EJBCHFLI",
"BCEFHIJK": "EJBCHFIK",
"BCEFGJKL": "EGBCJFLK",
"BCEFGIKL": "EGBCIFLK",
"BCEFGIJL": "EGBCJFLI",
"BCEFGIJK": "EGBCJFIK",
"BCEFGHKL": "EGBCHFLK",
"BCEFGHJL": "HGBCJFLE",
"BCEFGHJK": "HGBCJFEK",
"BCEFGHIL": "EGBCHFLI",
"BCEFGHIK": "EGBCHFIK",
"BCEFGHIJ": "HGBCJFEI",
"BCDHIJKL": "HJBCIDLK",
"BCDGIJKL": "IGBCJDLK",
"BCDGHJKL": "HGBCJDLK",
"BCDGHIKL": "HGBCIDLK",
"BCDGHIJL": "HGBCJDLI",
"BCDGHIJK": "HGBCJDIK",
"BCDFIJKL": "CJBDIFLK",
"BCDFHJKL": "CJBDHFLK",
"BCDFHIKL": "CIBDHFLK",
"BCDFHIJL": "CJBDHFLI",
"BCDFHIJK": "CJBDHFIK",
"BCDFGJKL": "CGBDJFLK",
"BCDFGIKL": "CGBDIFLK",
"BCDFGIJL": "CGBDJFLI",
"BCDFGIJK": "CGBDJFIK",
"BCDFGHKL": "CGBDHFLK",
"BCDFGHJL": "CGBDHFLJ",
"BCDFGHJK": "HGBCJFDK",
"BCDFGHIL": "CGBDHFLI",
"BCDFGHIK": "CGBDHFIK",
"BCDFGHIJ": "HGBCJFDI",
"BCDEIJKL": "EJBCIDLK",
"BCDEHJKL": "EJBCHDLK",
"BCDEHIKL": "EIBCHDLK",
"BCDEHIJL": "EJBCHDLI",
"BCDEHIJK": "EJBCHDIK",
"BCDEGJKL": "EGBCJDLK",
"BCDEGIKL": "EGBCIDLK",
"BCDEGIJL": "EGBCJDLI",
"BCDEGIJK": "EGBCJDIK",
"BCDEGHKL": "EGBCHDLK",
"BCDEGHJL": "HGBCJDLE",
"BCDEGHJK": "HGBCJDEK",
"BCDEGHIL": "EGBCHDLI",
"BCDEGHIK": "EGBCHDIK",
"BCDEGHIJ": "HGBCJDEI",
"BCDEFJKL": "CJBDEFLK",
"BCDEFIKL": "CEBDIFLK",
"BCDEFIJL": "CJBDEFLI",
"BCDEFIJK": "CJBDEFIK",
"BCDEFHKL": "CEBDHFLK",
"BCDEFHJL": "CJBDHFLE",
"BCDEFHJK": "CJBDHFEK",
"BCDEFHIL": "CEBDHFLI",
"BCDEFHIK": "CEBDHFIK",
"BCDEFHIJ": "CJBDHFEI",
"BCDEFGKL": "CGBDEFLK",
"BCDEFGJL": "CGBDJFLE",
"BCDEFGJK": "CGBDJFEK",
"BCDEFGIL": "CGBDEFLI",
"BCDEFGIK": "CGBDEFIK",
"BCDEFGIJ": "CGBDJFEI",
"BCDEFGHL": "CGBDHFLE",
"BCDEFGHK": "CGBDHFEK",
"BCDEFGHJ": "HGBCJFDE",
"BCDEFGHI": "CGBDHFEI",
"AFGHIJKL": "HJIFAGLK",
"AEGHIJKL": "EJIAHGLK",
"AEFHIJKL": "EJIFAHLK",
"AEFGIJKL": "EJIFAGLK",
"AEFGHJKL": "EGJFAHLK",
"AEFGHIKL": "EGIFAHLK",
"AEFGHIJL": "EGJFAHLI",
"AEFGHIJK": "EGJFAHIK",
"ADGHIJKL": "HJIDAGLK",
"ADFHIJKL": "HJIDAFLK",
"ADFGIJKL": "IGJDAFLK",
"ADFGHJKL": "HGJDAFLK",
"ADFGHIKL": "HGIDAFLK",
"ADFGHIJL": "HGJDAFLI",
"ADFGHIJK": "HGJDAFIK",
"ADEHIJKL": "EJIDAHLK",
"ADEGIJKL": "EJIDAGLK",
"ADEGHJKL": "EGJDAHLK",
"ADEGHIKL": "EGIDAHLK",
"ADEGHIJL": "EGJDAHLI",
"ADEGHIJK": "EGJDAHIK",
"ADEFIJKL": "EJIDAFLK",
"ADEFHJKL": "HJEDAFLK",
"ADEFHIKL": "HEIDAFLK",
"ADEFHIJL": "HJEDAFLI",
"ADEFHIJK": "HJEDAFIK",
"ADEFGJKL": "EGJDAFLK",
"ADEFGIKL": "EGIDAFLK",
"ADEFGIJL": "EGJDAFLI",
"ADEFGIJK": "EGJDAFIK",
"ADEFGHKL": "HGEDAFLK",
"ADEFGHJL": "HGJDAFLE",
"ADEFGHJK": "HGJDAFEK",
"ADEFGHIL": "HGEDAFLI",
"ADEFGHIK": "HGEDAFIK",
"ADEFGHIJ": "HGJDAFEI",
"ACGHIJKL": "HJICAGLK",
"ACFHIJKL": "HJICAFLK",
"ACFGIJKL": "IGJCAFLK",
"ACFGHJKL": "HGJCAFLK",
"ACFGHIKL": "HGICAFLK",
"ACFGHIJL": "HGJCAFLI",
"ACFGHIJK": "HGJCAFIK",
"ACEHIJKL": "EJICAHLK",
"ACEGIJKL": "EJICAGLK",
"ACEGHJKL": "EGJCAHLK",
"ACEGHIKL": "EGICAHLK",
"ACEGHIJL": "EGJCAHLI",
"ACEGHIJK": "EGJCAHIK",
"ACEFIJKL": "EJICAFLK",
"ACEFHJKL": "HJECAFLK",
"ACEFHIKL": "HEICAFLK",
"ACEFHIJL": "HJECAFLI",
"ACEFHIJK": "HJECAFIK",
"ACEFGJKL": "EGJCAFLK",
"ACEFGIKL": "EGICAFLK",
"ACEFGIJL": "EGJCAFLI",
"ACEFGIJK": "EGJCAFIK",
"ACEFGHKL": "HGECAFLK",
"ACEFGHJL": "HGJCAFLE",
"ACEFGHJK": "HGJCAFEK",
"ACEFGHIL": "HGECAFLI",
"ACEFGHIK": "HGECAFIK",
"ACEFGHIJ": "HGJCAFEI",
"ACDHIJKL": "HJICADLK",
"ACDGIJKL": "IGJCADLK",
"ACDGHJKL": "HGJCADLK",
"ACDGHIKL": "HGICADLK",
"ACDGHIJL": "HGJCADLI",
"ACDGHIJK": "HGJCADIK",
"ACDFIJKL": "CJIDAFLK",
"ACDFHJKL": "HJFCADLK",
"ACDFHIKL": "HFICADLK",
"ACDFHIJL": "HJFCADLI",
"ACDFHIJK": "HJFCADIK",
"ACDFGJKL": "CGJDAFLK",
"ACDFGIKL": "CGIDAFLK",
"ACDFGIJL": "CGJDAFLI",
"ACDFGIJK": "CGJDAFIK",
"ACDFGHKL": "HGFCADLK",
"ACDFGHJL": "CGJDAFLH",
"ACDFGHJK": "HGJCAFDK",
"ACDFGHIL": "HGFCADLI",
"ACDFGHIK": "HGFCADIK",
"ACDFGHIJ": "HGJCAFDI",
"ACDEIJKL": "EJICADLK",
"ACDEHJKL": "HJECADLK",
"ACDEHIKL": "HEICADLK",
"ACDEHIJL": "HJECADLI",
"ACDEHIJK": "HJECADIK",
"ACDEGJKL": "EGJCADLK",
"ACDEGIKL": "EGICADLK",
"ACDEGIJL": "EGJCADLI",
"ACDEGIJK": "EGJCADIK",
"ACDEGHKL": "HGECADLK",
"ACDEGHJL": "HGJCADLE",
"ACDEGHJK": "HGJCADEK",
"ACDEGHIL": "HGECADLI",
"ACDEGHIK": "HGECADIK",
"ACDEGHIJ": "HGJCADEI",
"ACDEFJKL": "CJEDAFLK",
"ACDEFIKL": "CEIDAFLK",
"ACDEFIJL": "CJEDAFLI",
"ACDEFIJK": "CJEDAFIK",
"ACDEFHKL": "HEFCADLK",
"ACDEFHJL": "HJFCADLE",
"ACDEFHJK": "HJECAFDK",
"ACDEFHIL": "HEFCADLI",
"ACDEFHIK": "HEFCADIK",
"ACDEFHIJ": "HJECAFDI",
"ACDEFGKL": "CGEDAFLK",
"ACDEFGJL": "CGJDAFLE",
"ACDEFGJK": "CGJDAFEK",
"ACDEFGIL": "CGEDAFLI",
"ACDEFGIK": "CGEDAFIK",
"ACDEFGIJ": "CGJDAFEI",
"ACDEFGHL": "HGFCADLE",
"ACDEFGHK": "HGECAFDK",
"ACDEFGHJ": "HGJCAFDE",
"ACDEFGHI": "HGECAFDI",
"ABGHIJKL": "HJBAIGLK",
"ABFHIJKL": "HJBAIFLK",
"ABFGIJKL": "IJBFAGLK",
"ABFGHJKL": "HJBFAGLK",
"ABFGHIKL": "HGBAIFLK",
"ABFGHIJL": "HJBFAGLI",
"ABFGHIJK": "HJBFAGIK",
"ABEHIJKL": "EJBAIHLK",
"ABEGIJKL": "EJBAIGLK",
"ABEGHJKL": "EJBAHGLK",
"ABEGHIKL": "EGBAIHLK",
"ABEGHIJL": "EJBAHGLI",
"ABEGHIJK": "EJBAHGIK",
"ABEFIJKL": "EJBAIFLK",
"ABEFHJKL": "EJBFAHLK",
"ABEFHIKL": "EIBFAHLK",
"ABEFHIJL": "EJBFAHLI",
"ABEFHIJK": "EJBFAHIK",
"ABEFGJKL": "EJBFAGLK",
"ABEFGIKL": "EGBAIFLK",
"ABEFGIJL": "EJBFAGLI",
"ABEFGIJK": "EJBFAGIK",
"ABEFGHKL": "EGBFAHLK",
"ABEFGHJL": "HJBFAGLE",
"ABEFGHJK": "HJBFAGEK",
"ABEFGHIL": "EGBFAHLI",
"ABEFGHIK": "EGBFAHIK",
"ABEFGHIJ": "HJBFAGEI",
"ABDHIJKL": "IJBDAHLK",
"ABDGIJKL": "IJBDAGLK",
"ABDGHJKL": "HJBDAGLK",
"ABDGHIKL": "IGBDAHLK",
"ABDGHIJL": "HJBDAGLI",
"ABDGHIJK": "HJBDAGIK",
"ABDFIJKL": "IJBDAFLK",
"ABDFHJKL": "HJBDAFLK",
"ABDFHIKL": "HIBDAFLK",
"ABDFHIJL": "HJBDAFLI",
"ABDFHIJK": "HJBDAFIK",
"ABDFGJKL": "FJBDAGLK",
"ABDFGIKL": "IGBDAFLK",
"ABDFGIJL": "FJBDAGLI",
"ABDFGIJK": "FJBDAGIK",
"ABDFGHKL": "HGBDAFLK",
"ABDFGHJL": "HGBDAFLJ",
"ABDFGHJK": "HGBDAFJK",
"ABDFGHIL": "HGBDAFLI",
"ABDFGHIK": "HGBDAFIK",
"ABDFGHIJ": "HGBDAFIJ",
"ABDEIJKL": "EJBAIDLK",
"ABDEHJKL": "EJBDAHLK",
"ABDEHIKL": "EIBDAHLK",
"ABDEHIJL": "EJBDAHLI",
"ABDEHIJK": "EJBDAHIK",
"ABDEGJKL": "EJBDAGLK",
"ABDEGIKL": "EGBAIDLK",
"ABDEGIJL": "EJBDAGLI",
"ABDEGIJK": "EJBDAGIK",
"ABDEGHKL": "EGBDAHLK",
"ABDEGHJL": "HJBDAGLE",
"ABDEGHJK": "HJBDAGEK",
"ABDEGHIL": "EGBDAHLI",
"ABDEGHIK": "EGBDAHIK",
"ABDEGHIJ": "HJBDAGEI",
"ABDEFJKL": "EJBDAFLK",
"ABDEFIKL": "EIBDAFLK",
"ABDEFIJL": "EJBDAFLI",
"ABDEFIJK": "EJBDAFIK",
"ABDEFHKL": "HEBDAFLK",
"ABDEFHJL": "HJBDAFLE",
"ABDEFHJK": "HJBDAFEK",
"ABDEFHIL": "HEBDAFLI",
"ABDEFHIK": "HEBDAFIK",
"ABDEFHIJ": "HJBDAFEI",
"ABDEFGKL": "EGBDAFLK",
"ABDEFGJL": "EGBDAFLJ",
"ABDEFGJK": "EGBDAFJK",
"ABDEFGIL": "EGBDAFLI",
"ABDEFGIK": "EGBDAFIK",
"ABDEFGIJ": "EGBDAFIJ",
"ABDEFGHL": "HGBDAFLE",
"ABDEFGHK": "HGBDAFEK",
"ABDEFGHJ": "HGBDAFEJ",
"ABDEFGHI": "HGBDAFEI",
"ABCHIJKL": "IJBCAHLK",
"ABCGIJKL": "IJBCAGLK",
"ABCGHJKL": "HJBCAGLK",
"ABCGHIKL": "IGBCAHLK",
"ABCGHIJL": "HJBCAGLI",
"ABCGHIJK": "HJBCAGIK",
"ABCFIJKL": "IJBCAFLK",
"ABCFHJKL": "HJBCAFLK",
"ABCFHIKL": "HIBCAFLK",
"ABCFHIJL": "HJBCAFLI",
"ABCFHIJK": "HJBCAFIK",
"ABCFGJKL": "CJBFAGLK",
"ABCFGIKL": "IGBCAFLK",
"ABCFGIJL": "CJBFAGLI",
"ABCFGIJK": "CJBFAGIK",
"ABCFGHKL": "HGBCAFLK",
"ABCFGHJL": "HGBCAFLJ",
"ABCFGHJK": "HGBCAFJK",
"ABCFGHIL": "HGBCAFLI",
"ABCFGHIK": "HGBCAFIK",
"ABCFGHIJ": "HGBCAFIJ",
"ABCEIJKL": "EJBAICLK",
"ABCEHJKL": "EJBCAHLK",
"ABCEHIKL": "EIBCAHLK",
"ABCEHIJL": "EJBCAHLI",
"ABCEHIJK": "EJBCAHIK",
"ABCEGJKL": "EJBCAGLK",
"ABCEGIKL": "EGBAICLK",
"ABCEGIJL": "EJBCAGLI",
"ABCEGIJK": "EJBCAGIK",
"ABCEGHKL": "EGBCAHLK",
"ABCEGHJL": "HJBCAGLE",
"ABCEGHJK": "HJBCAGEK",
"ABCEGHIL": "EGBCAHLI",
"ABCEGHIK": "EGBCAHIK",
"ABCEGHIJ": "HJBCAGEI",
"ABCEFJKL": "EJBCAFLK",
"ABCEFIKL": "EIBCAFLK",
"ABCEFIJL": "EJBCAFLI",
"ABCEFIJK": "EJBCAFIK",
"ABCEFHKL": "HEBCAFLK",
"ABCEFHJL": "HJBCAFLE",
"ABCEFHJK": "HJBCAFEK",
"ABCEFHIL": "HEBCAFLI",
"ABCEFHIK": "HEBCAFIK",
"ABCEFHIJ": "HJBCAFEI",
"ABCEFGKL": "EGBCAFLK",
"ABCEFGJL": "EGBCAFLJ",
"ABCEFGJK": "EGBCAFJK",
"ABCEFGIL": "EGBCAFLI",
"ABCEFGIK": "EGBCAFIK",
"ABCEFGIJ": "EGBCAFIJ",
"ABCEFGHL": "HGBCAFLE",
"ABCEFGHK": "HGBCAFEK",
"ABCEFGHJ": "HGBCAFEJ",
"ABCEFGHI": "HGBCAFEI",
"ABCDIJKL": "IJBCADLK",
"ABCDHJKL": "HJBCADLK",
"ABCDHIKL": "HIBCADLK",
"ABCDHIJL": "HJBCADLI",
"ABCDHIJK": "HJBCADIK",
"ABCDGJKL": "CJBDAGLK",
"ABCDGIKL": "IGBCADLK",
"ABCDGIJL": "CJBDAGLI",
"ABCDGIJK": "CJBDAGIK",
"ABCDGHKL": "HGBCADLK",
"ABCDGHJL": "HGBCADLJ",
"ABCDGHJK": "HGBCADJK",
"ABCDGHIL": "HGBCADLI",
"ABCDGHIK": "HGBCADIK",
"ABCDGHIJ": "HGBCADIJ",
"ABCDFJKL": "CJBDAFLK",
"ABCDFIKL": "CIBDAFLK",
"ABCDFIJL": "CJBDAFLI",
"ABCDFIJK": "CJBDAFIK",
"ABCDFHKL": "HFBCADLK",
"ABCDFHJL": "CJBDAFLH",
"ABCDFHJK": "HJBCAFDK",
"ABCDFHIL": "HFBCADLI",
"ABCDFHIK": "HFBCADIK",
"ABCDFHIJ": "HJBCAFDI",
"ABCDFGKL": "CGBDAFLK",
"ABCDFGJL": "CGBDAFLJ",
"ABCDFGJK": "CGBDAFJK",
"ABCDFGIL": "CGBDAFLI",
"ABCDFGIK": "CGBDAFIK",
"ABCDFGIJ": "CGBDAFIJ",
"ABCDFGHL": "CGBDAFLH",
"ABCDFGHK": "HGBCAFDK",
"ABCDFGHJ": "HGBCAFDJ",
"ABCDFGHI": "HGBCAFDI",
"ABCDEJKL": "EJBCADLK",
"ABCDEIKL": "EIBCADLK",
"ABCDEIJL": "EJBCADLI",
"ABCDEIJK": "EJBCADIK",
"ABCDEHKL": "HEBCADLK",
"ABCDEHJL": "HJBCADLE",
"ABCDEHJK": "HJBCADEK",
"ABCDEHIL": "HEBCADLI",
"ABCDEHIK": "HEBCADIK",
"ABCDEHIJ": "HJBCADEI",
"ABCDEGKL": "EGBCADLK",
"ABCDEGJL": "EGBCADLJ",
"ABCDEGJK": "EGBCADJK",
"ABCDEGIL": "EGBCADLI",
"ABCDEGIK": "EGBCADIK",
"ABCDEGIJ": "EGBCADIJ",
"ABCDEGHL": "HGBCADLE",
"ABCDEGHK": "HGBCADEK",
"ABCDEGHJ": "HGBCADEJ",
"ABCDEGHI": "HGBCADEI",
"ABCDEFKL": "CEBDAFLK",
"ABCDEFJL": "CJBDAFLE",
"ABCDEFJK": "CJBDAFEK",
"ABCDEFIL": "CEBDAFLI",
"ABCDEFIK": "CEBDAFIK",
"ABCDEFIJ": "CJBDAFEI",
"ABCDEFHL": "HFBCADLE",
"ABCDEFHK": "HEBCAFDK",
"ABCDEFHJ": "HJBCAFDE",
"ABCDEFHI": "HEBCAFDI",
"ABCDEFGL": "CGBDAFLE",
"ABCDEFGK": "CGBDAFEK",
"ABCDEFGJ": "CGBDAFEJ",
"ABCDEFGI": "CGBDAFEI",
"ABCDEFGH": "HGBCAFDE",
};

111
lib/bracket.ts Normal file
View File

@@ -0,0 +1,111 @@
import { ANNEX_C } from "./annexc-data";
import { GroupId, GroupTable, ThirdPlaceRow } from "./types";
// Die festen Paarungen der Runde der letzten 32 (FIFA-Spielplan, Annex zur Auslosung).
// Quelle: FIFA WM 2026 Wettbewerbsregeln, Spiele 73-88.
// "W"=Sieger, "R"=Zweiter, "3"=bester Drittplatzierter (Gruppe per Annex C bestimmt).
export interface BracketSlot {
type: "W" | "R" | "3";
group?: GroupId; // bei W/R: feste Gruppe
thirdPool?: GroupId[]; // bei 3: mögliche Quellgruppen (Info / UI)
}
export interface R32Match {
matchNumber: number;
home: BracketSlot;
away: BracketSlot;
}
// Die acht Gruppensieger, die einen Drittplatzierten bekommen — in der
// Spaltenreihenfolge von Annex C: 1A, 1B, 1D, 1E, 1G, 1I, 1K, 1L.
export const ANNEX_WINNER_ORDER: GroupId[] = ["A", "B", "D", "E", "G", "I", "K", "L"];
// Statische R32-Struktur. thirdPool dient nur der Anzeige vor Turnierende.
export const R32: R32Match[] = [
{ matchNumber: 73, home: { type: "R", group: "A" }, away: { type: "R", group: "B" } },
{ matchNumber: 74, home: { type: "W", group: "E" }, away: { type: "3", thirdPool: ["A","B","C","D","F"] } },
{ matchNumber: 75, home: { type: "W", group: "F" }, away: { type: "R", group: "C" } },
{ matchNumber: 76, home: { type: "W", group: "C" }, away: { type: "R", group: "F" } },
{ matchNumber: 77, home: { type: "W", group: "I" }, away: { type: "3", thirdPool: ["C","D","F","G","H"] } },
{ matchNumber: 78, home: { type: "R", group: "E" }, away: { type: "R", group: "I" } },
{ matchNumber: 79, home: { type: "W", group: "A" }, away: { type: "3", thirdPool: ["C","E","F","H","I"] } },
{ matchNumber: 80, home: { type: "W", group: "L" }, away: { type: "3", thirdPool: ["E","H","I","J","K"] } },
{ matchNumber: 81, home: { type: "W", group: "D" }, away: { type: "3", thirdPool: ["B","E","F","I","J"] } },
{ matchNumber: 82, home: { type: "W", group: "G" }, away: { type: "3", thirdPool: ["A","E","H","I","J"] } },
{ matchNumber: 83, home: { type: "R", group: "K" }, away: { type: "R", group: "L" } },
{ matchNumber: 84, home: { type: "W", group: "H" }, away: { type: "R", group: "J" } },
{ matchNumber: 85, home: { type: "W", group: "B" }, away: { type: "3", thirdPool: ["E","F","G","I","J"] } },
{ matchNumber: 86, home: { type: "W", group: "J" }, away: { type: "R", group: "H" } },
{ matchNumber: 87, home: { type: "W", group: "K" }, away: { type: "3", thirdPool: ["D","E","I","J","L"] } },
{ matchNumber: 88, home: { type: "R", group: "D" }, away: { type: "R", group: "G" } },
];
// Folgerunden: jedes Spiel speist sich aus zwei Vorspielen (FIFA-Spielplan).
export interface KnockoutMatch {
matchNumber: number;
stage: "R16" | "QF" | "SF" | "3RD" | "FINAL";
fromHome: number; // Match-Nr des Vorspiels (Sieger), bei 3RD: Verlierer
fromAway: number;
losers?: boolean; // true beim Spiel um Platz 3
}
export const LATER_ROUNDS: KnockoutMatch[] = [
{ matchNumber: 89, stage: "R16", fromHome: 74, fromAway: 77 },
{ matchNumber: 90, stage: "R16", fromHome: 73, fromAway: 75 },
{ matchNumber: 91, stage: "R16", fromHome: 76, fromAway: 78 },
{ matchNumber: 92, stage: "R16", fromHome: 79, fromAway: 80 },
{ matchNumber: 93, stage: "R16", fromHome: 83, fromAway: 84 },
{ matchNumber: 94, stage: "R16", fromHome: 81, fromAway: 82 },
{ matchNumber: 95, stage: "R16", fromHome: 86, fromAway: 88 },
{ matchNumber: 96, stage: "R16", fromHome: 85, fromAway: 87 },
{ matchNumber: 97, stage: "QF", fromHome: 89, fromAway: 90 },
{ matchNumber: 98, stage: "QF", fromHome: 93, fromAway: 94 },
{ matchNumber: 99, stage: "QF", fromHome: 91, fromAway: 92 },
{ matchNumber: 100, stage: "QF", fromHome: 95, fromAway: 96 },
{ matchNumber: 101, stage: "SF", fromHome: 97, fromAway: 98 },
{ matchNumber: 102, stage: "SF", fromHome: 99, fromAway: 100 },
{ matchNumber: 103, stage: "3RD", fromHome: 101, fromAway: 102, losers: true },
{ matchNumber: 104, stage: "FINAL", fromHome: 101, fromAway: 102 },
];
// Ergebnis der Annex-C-Auflösung: pro Gruppensieger der zugeordnete Dritte.
export type ThirdAssignment = Partial<Record<GroupId, GroupId>>;
// Normalisiert die qualifizierten Gruppen zum Annex-C-Schlüssel (alphabetisch).
export function annexKey(qualifiedGroups: GroupId[]): string {
return [...qualifiedGroups].sort().join("");
}
// Schlägt die Zuordnung in Annex C nach. Gibt {Gruppensieger -> 3.Gruppe} zurück.
// Liefert null, wenn nicht genau 8 Gruppen oder kein Treffer.
export function resolveAnnexC(qualifiedGroups: GroupId[]): ThirdAssignment | null {
if (qualifiedGroups.length !== 8) return null;
const key = annexKey(qualifiedGroups);
const value = ANNEX_C[key];
if (!value) return null;
const assignment: ThirdAssignment = {};
for (let i = 0; i < ANNEX_WINNER_ORDER.length; i++) {
assignment[ANNEX_WINNER_ORDER[i]] = value[i] as GroupId;
}
return assignment;
}
// Bestimmt die qualifizierten Drittplatzierten-Gruppen aus der Tabelle.
export function qualifiedThirdGroups(thirds: ThirdPlaceRow[]): GroupId[] {
return thirds.filter((t) => t.qualifies).map((t) => t.group);
}
// Hilfslabel für einen Slot, abhängig davon ob die Annex-C-Zuordnung schon feststeht.
export function slotLabel(
slot: BracketSlot,
assignment: ThirdAssignment | null,
winnerGroupForThisMatch?: GroupId,
): string {
if (slot.type === "W") return `Sieger ${slot.group}`;
if (slot.type === "R") return `Zweiter ${slot.group}`;
// 3. Platz
if (assignment && winnerGroupForThisMatch && assignment[winnerGroupForThisMatch]) {
return `3. der Gruppe ${assignment[winnerGroupForThisMatch]}`;
}
return `3. ${slot.thirdPool?.join("/") ?? "?"}`;
}

199
lib/feeds.ts Normal file
View File

@@ -0,0 +1,199 @@
import { GroupId, Match, MatchStatus, Team } from "./types";
// ----------------------------------------------------------------------------
// Caching: einfacher In-Memory-Cache mit TTL. Verhindert, dass jede Browser-
// Anfrage einen Upstream-Call auslöst (football-data: 10 req/min Limit).
// ----------------------------------------------------------------------------
interface CacheEntry<T> { value: T; expires: number; }
const cache = new Map<string, CacheEntry<unknown>>();
export async function cached<T>(key: string, ttlMs: number, fn: () => Promise<T>): Promise<T> {
const hit = cache.get(key) as CacheEntry<T> | undefined;
const now = Date.now();
if (hit && hit.expires > now) return hit.value;
try {
const value = await fn();
cache.set(key, { value, expires: now + ttlMs });
return value;
} catch (err) {
// Bei Upstream-Fehler abgelaufenen Cache weiterverwenden, statt hart zu failen.
if (hit) return hit.value;
throw err;
}
}
// ----------------------------------------------------------------------------
// football-data.org: Teams, Spiele, Status
// ----------------------------------------------------------------------------
const FD_BASE = "https://api.football-data.org/v4";
const FD_COMP = "WC"; // FIFA World Cup
function fdHeaders(): Record<string, string> {
const token = process.env.FOOTBALL_DATA_TOKEN;
return token ? { "X-Auth-Token": token } : {};
}
function mapStatus(s: string): MatchStatus {
switch (s) {
case "LIVE": return "LIVE";
case "IN_PLAY": return "IN_PLAY";
case "PAUSED": return "PAUSED";
case "FINISHED": return "FINISHED";
default: return "SCHEDULED";
}
}
// Wandelt einen football-data-Gruppennamen ("GROUP_A") in unsere GroupId.
function parseGroup(g: string | null | undefined): GroupId | null {
if (!g) return null;
const m = /GROUP_([A-L])/.exec(g);
return m ? (m[1] as GroupId) : null;
}
interface FdMatchesResponse {
matches: Array<{
id: number;
utcDate: string;
status: string;
minute?: number | null;
matchday?: number | null;
stage: string;
group?: string | null;
homeTeam: { id: number | null; name: string | null; tla?: string | null };
awayTeam: { id: number | null; name: string | null; tla?: string | null };
score: { fullTime: { home: number | null; away: number | null } };
}>;
}
function stageFor(stage: string, group: GroupId | null): Match["stage"] {
if (group) return "GROUP";
switch (stage) {
case "LAST_32": return "R32";
case "LAST_16": return "R16";
case "QUARTER_FINALS": return "QF";
case "SEMI_FINALS": return "SF";
case "THIRD_PLACE": return "3RD";
case "FINAL": return "FINAL";
default: return "GROUP";
}
}
// Holt alle Spiele + leitet die Teamliste daraus ab (spart einen Extra-Call).
export async function fetchMatchesAndTeams(): Promise<{ matches: Match[]; teams: Team[] }> {
return cached("fd:matches", 60_000, async () => {
const res = await fetch(`${FD_BASE}/competitions/${FD_COMP}/matches`, {
headers: fdHeaders(),
// Next.js: kein eigenes Caching, wir cachen selbst
cache: "no-store",
});
if (!res.ok) throw new Error(`football-data ${res.status}`);
const data = (await res.json()) as FdMatchesResponse;
const teamMap = new Map<string, Team>();
const matches: Match[] = data.matches.map((m) => {
const group = parseGroup(m.group);
// Teams registrieren (nur wenn Gruppenspiel und ID vorhanden)
for (const side of [m.homeTeam, m.awayTeam]) {
if (side.id != null && side.name && group) {
const id = String(side.id);
if (!teamMap.has(id)) {
teamMap.set(id, { id, name: side.name, code: side.tla ?? "", group });
}
}
}
return {
id: String(m.id),
group,
stage: stageFor(m.stage, group),
matchNumber: m.matchday ?? 0,
utcDate: m.utcDate,
status: mapStatus(m.status),
minute: m.minute ?? null,
homeTeamId: m.homeTeam.id != null ? String(m.homeTeam.id) : null,
awayTeamId: m.awayTeam.id != null ? String(m.awayTeam.id) : null,
homeScore: m.score.fullTime.home,
awayScore: m.score.fullTime.away,
};
});
return { matches, teams: [...teamMap.values()] };
});
}
// ----------------------------------------------------------------------------
// Polymarket Gamma API: Wahrscheinlichkeiten pro Spiel
// ----------------------------------------------------------------------------
const PM_BASE = "https://gamma-api.polymarket.com";
interface PmMarket {
question: string;
outcomes: string; // JSON-String, z.B. "[\"Germany\",\"Draw\",\"Mexico\"]"
outcomePrices: string; // JSON-String, z.B. "[\"0.55\",\"0.25\",\"0.20\"]"
slug: string;
closed: boolean;
}
// Map: normalisierter Teamname-Schlüssel -> Wahrscheinlichkeit.
export interface OddsEntry {
outcomes: string[];
prices: number[];
question: string;
}
function safeParse<T>(s: string, fallback: T): T {
try { return JSON.parse(s) as T; } catch { return fallback; }
}
// Holt WM-bezogene Märkte über das Tag/Slug der Polymarket-WM-Kollektion.
// Hinweis: Der exakte Slug kann sich ändern; per ENV überschreibbar.
export async function fetchOdds(): Promise<OddsEntry[]> {
const slug = process.env.POLYMARKET_WC_SLUG || "world-cup-2026";
return cached("pm:odds", 120_000, async () => {
const res = await fetch(
`${PM_BASE}/events?slug=${encodeURIComponent(slug)}`,
{ cache: "no-store", headers: { "User-Agent": "wm2026-board/1.0" } },
);
if (!res.ok) throw new Error(`polymarket ${res.status}`);
const events = (await res.json()) as Array<{ markets?: PmMarket[] }>;
const entries: OddsEntry[] = [];
for (const ev of events) {
for (const mk of ev.markets ?? []) {
if (mk.closed) continue;
const outcomes = safeParse<string[]>(mk.outcomes, []);
const prices = safeParse<string[]>(mk.outcomePrices, []).map(Number);
if (outcomes.length && outcomes.length === prices.length) {
entries.push({ outcomes, prices, question: mk.question });
}
}
}
return entries;
});
}
// Verknüpft Polymarket-Märkte mit Spielen anhand der Teamnamen im Markttitel.
export function attachOdds(matches: Match[], teams: Team[], odds: OddsEntry[]): Match[] {
const nameById = new Map(teams.map((t) => [t.id, t.name.toLowerCase()]));
return matches.map((m) => {
if (!m.homeTeamId || !m.awayTeamId) return m;
const hn = nameById.get(m.homeTeamId);
const an = nameById.get(m.awayTeamId);
if (!hn || !an) return m;
const hit = odds.find(
(o) => o.question.toLowerCase().includes(hn) && o.question.toLowerCase().includes(an),
);
if (!hit) return m;
// Heuristik: 3 Outcomes = Heim/Unentschieden/Auswärts; 2 = Heim/Auswärts.
const idxHome = hit.outcomes.findIndex((o) => o.toLowerCase().includes(hn));
const idxAway = hit.outcomes.findIndex((o) => o.toLowerCase().includes(an));
const idxDraw = hit.outcomes.findIndex((o) => /draw|unentschieden|tie/i.test(o));
if (idxHome < 0 || idxAway < 0) return m;
return {
...m,
prob: {
home: hit.prices[idxHome] ?? 0,
draw: idxDraw >= 0 ? (hit.prices[idxDraw] ?? null) : null,
away: hit.prices[idxAway] ?? 0,
},
};
});
}

140
lib/resolve-bracket.ts Normal file
View File

@@ -0,0 +1,140 @@
import { GroupId, GroupTable, Match, Team, ThirdPlaceRow } from "@/lib/types";
import {
R32, LATER_ROUNDS, R32Match, BracketSlot,
ThirdAssignment, slotLabel,
} from "@/lib/bracket";
// Ein aufgelöster Teilnehmer einer K.o.-Begegnung.
export interface ResolvedSide {
teamId: string | null; // null = noch Platzhalter
label: string; // Anzeige (Teamname oder "Sieger E" etc.)
code: string;
score: number | null;
isWinner: boolean;
}
export interface ResolvedTie {
matchNumber: number;
stage: string;
home: ResolvedSide;
away: ResolvedSide;
status: string;
prob?: { home: number; draw: number | null; away: number } | null;
}
// Findet das tatsächliche Spiel (aus dem Feed) zu einer FIFA-Match-Nummer.
function feedMatch(matches: Match[], num: number): Match | undefined {
return matches.find((m) => m.matchNumber === num && m.group == null);
}
// Bestimmt den Sieger eines abgeschlossenen Spiels (Feed) als Team-ID.
function winnerOf(m: Match | undefined): string | null {
if (!m || m.status !== "FINISHED") return null;
if (m.homeScore == null || m.awayScore == null) return null;
if (m.homeScore > m.awayScore) return m.homeTeamId;
if (m.awayScore > m.homeScore) return m.awayTeamId;
return null; // Unentschieden -> Elfmeter; Feed liefert i.d.R. Sieger separat
}
function loserOf(m: Match | undefined): string | null {
if (!m || m.status !== "FINISHED") return null;
if (m.homeScore == null || m.awayScore == null) return null;
if (m.homeScore > m.awayScore) return m.awayTeamId;
if (m.awayScore > m.homeScore) return m.homeTeamId;
return null;
}
// Löst einen R32-Slot (W/R/3) zu einer Team-ID auf, sofern bereits bekannt.
function resolveR32Slot(
slot: BracketSlot,
winnerGroup: GroupId | undefined,
tables: GroupTable[],
assignment: ThirdAssignment | null,
thirds: ThirdPlaceRow[],
): string | null {
const table = (g: GroupId) => tables.find((t) => t.group === g);
if (slot.type === "W") {
const t = table(slot.group!);
return t?.rows.find((r) => r.rank === 1)?.teamId ?? null;
}
if (slot.type === "R") {
const t = table(slot.group!);
return t?.rows.find((r) => r.rank === 2)?.teamId ?? null;
}
// 3. Platz: braucht Annex-C-Zuordnung + den Gruppensieger dieses Spiels
if (slot.type === "3" && assignment && winnerGroup) {
const thirdGroup = assignment[winnerGroup];
if (thirdGroup) {
const row = thirds.find((r) => r.group === thirdGroup && r.qualifies);
return row?.teamId ?? null;
}
}
return null;
}
function sideFrom(
teamId: string | null, fallbackLabel: string,
teams: Team[], feed: Match | undefined, which: "home" | "away",
): ResolvedSide {
const team = teamId ? teams.find((t) => t.id === teamId) : undefined;
const score = feed
? which === "home" ? feed.homeScore : feed.awayScore
: null;
const w = winnerOf(feed);
return {
teamId,
label: team?.name ?? fallbackLabel,
code: team?.code ?? "",
score,
isWinner: teamId != null && w === teamId,
};
}
// Baut die komplette aufgelöste Bracket-Struktur.
export function resolveBracket(
matches: Match[], teams: Team[], tables: GroupTable[],
thirds: ThirdPlaceRow[], assignment: ThirdAssignment | null,
): { r32: ResolvedTie[]; later: Record<number, ResolvedTie> } {
// Map: Match-Nummer -> Sieger-Team-ID (für Propagation in Folgerunden)
const winners = new Map<number, string | null>();
const losers = new Map<number, string | null>();
const r32: ResolvedTie[] = R32.map((rm: R32Match) => {
const feed = feedMatch(matches, rm.matchNumber);
const homeGroup = rm.home.type === "W" ? rm.home.group : undefined;
const awayGroup = rm.away.type === "W" ? rm.away.group : undefined;
// Für 3.-Platz-Slots: Gruppensieger ist die andere Seite (immer W)
const winnerGroup = (homeGroup ?? awayGroup) as GroupId | undefined;
const homeId = resolveR32Slot(rm.home, winnerGroup, tables, assignment, thirds);
const awayId = resolveR32Slot(rm.away, winnerGroup, tables, assignment, thirds);
const home = sideFrom(homeId, slotLabel(rm.home, assignment, winnerGroup), teams, feed, "home");
const away = sideFrom(awayId, slotLabel(rm.away, assignment, winnerGroup), teams, feed, "away");
winners.set(rm.matchNumber, winnerOf(feed));
losers.set(rm.matchNumber, loserOf(feed));
return {
matchNumber: rm.matchNumber, stage: "R32",
home, away, status: feed?.status ?? "SCHEDULED", prob: feed?.prob,
};
});
const later: Record<number, ResolvedTie> = {};
for (const km of LATER_ROUNDS) {
const feed = feedMatch(matches, km.matchNumber);
const src = km.losers ? losers : winners;
const homeId = src.get(km.fromHome) ?? null;
const awayId = src.get(km.fromAway) ?? null;
const homeLabel = `${km.losers ? "Verlierer" : "Sieger"} ${km.fromHome}`;
const awayLabel = `${km.losers ? "Verlierer" : "Sieger"} ${km.fromAway}`;
const home = sideFrom(homeId, homeLabel, teams, feed, "home");
const away = sideFrom(awayId, awayLabel, teams, feed, "away");
winners.set(km.matchNumber, winnerOf(feed));
losers.set(km.matchNumber, loserOf(feed));
later[km.matchNumber] = {
matchNumber: km.matchNumber, stage: km.stage,
home, away, status: feed?.status ?? "SCHEDULED", prob: feed?.prob,
};
}
return { r32, later };
}

131
lib/standings.ts Normal file
View File

@@ -0,0 +1,131 @@
import {
GROUP_IDS, GroupId, Match, Team,
StandingRow, GroupTable, ThirdPlaceRow,
} from "./types";
// Berechnet eine leere Tabellenzeile.
function emptyRow(teamId: string): StandingRow {
return {
teamId, played: 0, won: 0, drawn: 0, lost: 0,
goalsFor: 0, goalsAgainst: 0, goalDiff: 0, points: 0, rank: 0,
};
}
// Trägt ein abgeschlossenes Spiel in zwei Tabellenzeilen ein.
function applyMatch(rows: Map<string, StandingRow>, m: Match) {
if (m.status !== "FINISHED") return;
if (m.homeTeamId == null || m.awayTeamId == null) return;
if (m.homeScore == null || m.awayScore == null) return;
const h = rows.get(m.homeTeamId);
const a = rows.get(m.awayTeamId);
if (!h || !a) return;
h.played++; a.played++;
h.goalsFor += m.homeScore; h.goalsAgainst += m.awayScore;
a.goalsFor += m.awayScore; a.goalsAgainst += m.homeScore;
if (m.homeScore > m.awayScore) { h.won++; a.lost++; h.points += 3; }
else if (m.homeScore < m.awayScore) { a.won++; h.lost++; a.points += 3; }
else { h.drawn++; a.drawn++; h.points += 1; a.points += 1; }
}
function finalizeRow(r: StandingRow) {
r.goalDiff = r.goalsFor - r.goalsAgainst;
}
// Vergleicht zwei Teams nach FIFA-Kriterien innerhalb derselben Gruppe.
// Reihenfolge: Punkte, dann Direktvergleich (Punkte/TD/Tore in den Spielen
// der punktgleichen Teams), dann Gesamt-TD, Gesamt-Tore. Fair-Play/Ranking
// werden hier nicht abgebildet (kein Feed dafür) -> bleibt bei Gleichstand stabil.
function compareInGroup(
a: StandingRow, b: StandingRow,
matches: Match[], tiedIds: Set<string>,
): number {
if (b.points !== a.points) return b.points - a.points;
// Direktvergleich nur unter den punktgleichen Teams
if (tiedIds.size >= 2 && tiedIds.has(a.teamId) && tiedIds.has(b.teamId)) {
const mini = miniTable(matches, tiedIds);
const ma = mini.get(a.teamId);
const mb = mini.get(b.teamId);
if (ma && mb) {
if (mb.points !== ma.points) return mb.points - ma.points;
const mdA = ma.goalsFor - ma.goalsAgainst;
const mdB = mb.goalsFor - mb.goalsAgainst;
if (mdB !== mdA) return mdB - mdA;
if (mb.goalsFor !== ma.goalsFor) return mb.goalsFor - ma.goalsFor;
}
}
if (b.goalDiff !== a.goalDiff) return b.goalDiff - a.goalDiff;
if (b.goalsFor !== a.goalsFor) return b.goalsFor - a.goalsFor;
return 0;
}
// Baut eine Minitabelle aus den Spielen, in denen NUR die tiedIds gegeneinander spielten.
function miniTable(matches: Match[], tiedIds: Set<string>): Map<string, StandingRow> {
const rows = new Map<string, StandingRow>();
tiedIds.forEach((id) => rows.set(id, emptyRow(id)));
for (const m of matches) {
if (m.status !== "FINISHED") continue;
if (m.homeTeamId == null || m.awayTeamId == null) continue;
if (!tiedIds.has(m.homeTeamId) || !tiedIds.has(m.awayTeamId)) continue;
applyMatch(rows, m);
}
rows.forEach(finalizeRow);
return rows;
}
// Berechnet alle 12 Gruppentabellen aus Teams + Spielen.
export function computeGroupTables(teams: Team[], matches: Match[]): GroupTable[] {
const tables: GroupTable[] = [];
for (const g of GROUP_IDS) {
const groupTeams = teams.filter((t) => t.group === g);
const rows = new Map<string, StandingRow>();
groupTeams.forEach((t) => rows.set(t.id, emptyRow(t.id)));
const groupMatches = matches.filter((m) => m.group === g);
for (const m of groupMatches) applyMatch(rows, m);
rows.forEach(finalizeRow);
const arr = [...rows.values()];
// Gruppen punktgleicher Teams für den Direktvergleich bestimmen
const byPoints = new Map<number, string[]>();
arr.forEach((r) => {
const list = byPoints.get(r.points) ?? [];
list.push(r.teamId);
byPoints.set(r.points, list);
});
arr.sort((a, b) => {
const tied = byPoints.get(a.points) ?? [];
const tiedIds = new Set(a.points === b.points && tied.length >= 2 ? tied : []);
return compareInGroup(a, b, groupMatches, tiedIds);
});
arr.forEach((r, i) => (r.rank = i + 1));
tables.push({ group: g, rows: arr });
}
return tables;
}
// Rankt die 12 Gruppendritten gruppenübergreifend (ohne Direktvergleich).
// Kriterien: Punkte, TD, Tore. Beste 8 -> qualifies = true.
export function computeThirdPlaceTable(tables: GroupTable[]): ThirdPlaceRow[] {
const thirds: ThirdPlaceRow[] = tables
.map((t) => {
const r = t.rows.find((x) => x.rank === 3);
if (!r) return null;
return { ...r, group: t.group, qualifies: false, overallRank: 0 };
})
.filter((x): x is ThirdPlaceRow => x !== null);
thirds.sort((a, b) => {
if (b.points !== a.points) return b.points - a.points;
if (b.goalDiff !== a.goalDiff) return b.goalDiff - a.goalDiff;
if (b.goalsFor !== a.goalsFor) return b.goalsFor - a.goalsFor;
return a.group.localeCompare(b.group); // stabil
});
thirds.forEach((r, i) => {
r.overallRank = i + 1;
r.qualifies = i < 8;
});
return thirds;
}

62
lib/types.ts Normal file
View File

@@ -0,0 +1,62 @@
// Datenmodell für die WM-Seite. Bewusst entkoppelt von den Feed-Formaten:
// Adapter (siehe lib/feeds.ts) übersetzen football-data.org & Polymarket in diese Typen.
export type GroupId =
| "A" | "B" | "C" | "D" | "E" | "F"
| "G" | "H" | "I" | "J" | "K" | "L";
export const GROUP_IDS: GroupId[] = [
"A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L",
];
export interface Team {
id: string; // stabile ID aus dem Feed
name: string; // Anzeigename
code: string; // 3-Buchstaben-Code, z.B. GER
group: GroupId;
}
export type MatchStatus =
| "SCHEDULED" | "LIVE" | "IN_PLAY" | "PAUSED" | "FINISHED";
export interface Match {
id: string;
group: GroupId | null; // null = K.o.-Spiel
stage: "GROUP" | "R32" | "R16" | "QF" | "SF" | "3RD" | "FINAL";
matchNumber: number; // FIFA-Spielnummer (1..104)
utcDate: string;
status: MatchStatus;
minute: number | null;
homeTeamId: string | null;
awayTeamId: string | null;
homeScore: number | null;
awayScore: number | null;
// Wahrscheinlichkeiten 0..1 (Polymarket), falls vorhanden
prob?: { home: number; draw: number | null; away: number } | null;
}
// Eine berechnete Tabellenzeile innerhalb einer Gruppe.
export interface StandingRow {
teamId: string;
played: number;
won: number;
drawn: number;
lost: number;
goalsFor: number;
goalsAgainst: number;
goalDiff: number;
points: number;
rank: number; // 1..4 innerhalb der Gruppe
}
export interface GroupTable {
group: GroupId;
rows: StandingRow[]; // sortiert, rank gesetzt
}
// Eintrag in der gruppenübergreifenden Drittplatzierten-Tabelle.
export interface ThirdPlaceRow extends StandingRow {
group: GroupId;
qualifies: boolean; // true für die besten 8
overallRank: number; // 1..12
}