init
This commit is contained in:
500
lib/annexc-data.ts
Normal file
500
lib/annexc-data.ts
Normal 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
111
lib/bracket.ts
Normal 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
199
lib/feeds.ts
Normal 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
140
lib/resolve-bracket.ts
Normal 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
131
lib/standings.ts
Normal 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
62
lib/types.ts
Normal 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
|
||||
}
|
||||
Reference in New Issue
Block a user